Skip to content

Q2R reversible Ising automaton

The Q2R automaton, a reversible, energy-conserving Ising dynamics on a periodic lattice of even sides.

  • Name: q2r
  • Field: physics/statistical_mechanics
  • Formalism: lattice dynamics
  • References: Vichniac 1984; Pomeau 1984; Lindgren 2024

Factory

@register_model(
    field='physics/statistical_mechanics',
    tags=('lattice', 'cellular automaton', 'reversible', 'spins'),
    references=('Vichniac 1984', 'Pomeau 1984', 'Lindgren 2024'),
    defaults={'until': 100, 'dt': 1},
)
def q2r(
    shape: Sequence[int] = (64, 64),
    *,
    up: float = 0.3,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Q2R automaton, a reversible, energy-conserving Ising dynamics on a periodic lattice of even sides.

    The spins start uncorrelated with a fraction `up` of spins up, and the sites of even coordinate sum update first.

    Args:
        shape:
            The number of sites along each axis, all even.

        up:
            The probability that an initial spin is up.

        random_state:
            The seed or generator of the initial spins.

    Returns:
        system:
            The model, with the spins `spins.spin` and phases `spins.phase`.

    Examples:
        >>> from skcomplex.models.physics.statistical_mechanics import q2r
        >>> q2r((8,)).elements[0].check().prepare().size_
        8
    """
    shape = tuple(shape)
    spins = np.where(np.random.default_rng(random_state).random(shape) < up, 1.0, -1.0)
    space = LatticeSpace(shape)
    group = LatticeElement(
        'spins',
        element=Q2RSpinElement,
        space=space,
        values={'spin': spins, 'phase': _checkerboard(shape)},
    )
    flip = LocalRuleInteraction(
        'flip',
        target='spins',
        state=('spin', 'phase'),
        rule=_flip_without_cost,
        neighborhood=VonNeumannNeighborhood(n_dim=len(shape), center=True),
    )
    return System(elements=[group], interactions=[flip])

Components

class Q2RSpinElement(Element):
    """Site of the Q2R automaton, a spin and a phase that says whether the site updates in the current step.

    The local rule of Q2R flips the spins of the updating sites whose neighbours sum to zero, which leaves the Ising
    energy unchanged, and swaps the phase of every site, so the two sublattices update in turn and the dynamics is
    reversible.

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

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

        spin:
            The initial spin, 1 or -1.

        phase:
            The initial phase, 1 for updating and 0 for quiescent.

    Examples:
        >>> from skcomplex.models.physics.statistical_mechanics import Q2RSpinElement
        >>> Q2RSpinElement('site').phase
        0.0
    """

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

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.

    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.interactions import LocalRuleInteraction
        >>> from skcomplex.core.spaces import VonNeumannNeighborhood
        >>> from skcomplex.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.check().schedule
        'synchronous'
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        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,
            ),
        ]