Skip to content

Nonreciprocal Cahn-Hilliard

The nonreciprocal Cahn-Hilliard equations, a demixing field coupled to a diffusing one.

  • Name: nonreciprocal_cahn_hilliard
  • Field: physics/pattern_formation
  • Formalism: ordinary differential equations
  • References: Brauns and Marchetti 2024; Saha, Agudo-Canalejo, and Golestanian 2020; Frohoff-Hülsmann 2021

Factory

@register_model(
    field='physics/pattern_formation',
    tags=('field', 'conserved', 'nonreciprocal', 'traveling waves', 'phase separation', 'pde'),
    references=('Brauns and Marchetti 2024', 'Saha, Agudo-Canalejo, and Golestanian 2020', 'Frohoff-Hülsmann 2021'),
    defaults={'until': 400.0, 'dt': 2.0},
)
def nonreciprocal_cahn_hilliard(
    shape: Sequence[int] = (128,),
    *,
    length: float = 40.0,
    chi: float = 0.0,
    alpha: float = 0.3,
    D: float = 0.1,
    kappa: float = 1.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the nonreciprocal Cahn-Hilliard equations, a demixing field coupled to a diffusing one.

    The conserved field `phi` phase separates and the conserved field `psi` diffuses, and they are coupled through
    cross-diffusion, `phi_t = laplacian(-phi + phi^3 - kappa laplacian(phi) + (chi + alpha) psi)` and
    `psi_t = laplacian(D psi + (chi - alpha) phi)`, the minimal model of Brauns and Marchetti. The coupling `chi` is
    reciprocal and `alpha` is not. When `alpha^2 - chi^2` exceeds `D^2`, the unstable band of the dispersion relation
    of the mixed state reaches the propagating modes, and the demixed domains travel instead of coarsening.

    Args:
        shape:
            The number of sites along every axis of the periodic lattice.

        length:
            The side of the domain.

        chi:
            The reciprocal part of the cross-diffusion.

        alpha:
            The nonreciprocal part of the cross-diffusion.

        D:
            The diffusivity of `psi`.

        kappa:
            The stiffness of the interfaces of `phi`.

        random_state:
            The seed or generator of the small random initial fields around the mixed state.

    Returns:
        system:
            The model, with the demixing field `mixture.phi` and the diffusing field `mixture.psi`.

    Examples:
        >>> from skcomplex.models.physics.pattern_formation import nonreciprocal_cahn_hilliard
        >>> nonreciprocal_cahn_hilliard((16,), alpha=0.5).elements[0].values['alpha']
        0.5
    """
    shape = tuple(shape)
    rng = np.random.default_rng(random_state)
    values = {
        'phi': 0.01 * rng.uniform(-1.0, 1.0, shape),
        'psi': 0.01 * rng.uniform(-1.0, 1.0, shape),
        'chi': chi,
        'alpha': alpha,
        'D': D,
        'kappa': kappa,
    }
    space = LatticeSpace(shape, spacing=length / shape[0])
    return System(elements=[FieldElement('mixture', element=NonreciprocalMixtureElement, space=space, values=values)])

Components

class NonreciprocalMixtureElement(Element):
    """Site of two conserved fields coupled nonreciprocally, a demixing field `phi` and a diffusing field `psi`.

    The fields follow `phi_t = laplacian(-phi + phi^3 - kappa laplacian(phi) + (chi + alpha) psi)` and
    `psi_t = laplacian(D psi + (chi - alpha) phi)`. The reciprocal coupling `chi` derives from a free energy, and the
    nonreciprocal coupling `alpha` does not, so above a threshold the demixed domains travel.

    Args:
        label:
            The label of the site or the field.

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

        phi:
            The initial demixing field.

        psi:
            The initial diffusing field.

        chi:
            The reciprocal coupling.

        alpha:
            The nonreciprocal coupling.

        D:
            The diffusion coefficient of `psi`.

        kappa:
            The stiffness of the interfaces of `phi`.

    Attributes:
        default_visual_:
            The visual of the site when it sets none.

    Examples:
        >>> from skcomplex.models.physics.pattern_formation import NonreciprocalMixtureElement
        >>> NonreciprocalMixtureElement('site').alpha
        0.3
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        phi: Annotated[float, StateSpec()] = 0.0,
        psi: Annotated[float, StateSpec()] = 0.0,
        chi: Annotated[float, ParameterSpec()] = 0.0,
        alpha: Annotated[float, ParameterSpec()] = 0.3,
        D: Annotated[float, ParameterSpec()] = 0.1,
        kappa: Annotated[float, ParameterSpec()] = 1.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.phi = phi
        self.psi = psi
        self.chi = chi
        self.alpha = alpha
        self.D = D
        self.kappa = kappa

    def _get_laws(self, namespace: SimpleNamespace) -> list[BaseLaw]:
        """Return the conserved rates of both fields.

        Args:
            namespace:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The rates of `phi` and `psi`.
        """
        s = namespace
        potential = -s.phi + s.phi**3 - s.kappa * laplacian(s.phi) + (s.chi + s.alpha) * s.psi
        return [
            RateLaw(s.phi, laplacian(potential)),
            RateLaw(s.psi, laplacian(s.D * s.psi + (s.chi - s.alpha) * s.phi)),
        ]

Equations

mixture.phi[i]: diff(phi_mixture(t), t) == laplacian(phi_mixture(t)^3 - kappa_mixture*laplacian(phi_mixture(t)) + (alpha_mixture + chi_mixture)*psi_mixture(t) - phi_mixture(t))
mixture.psi[i]: diff(psi_mixture(t), t) == laplacian(-(alpha_mixture - chi_mixture)*phi_mixture(t) + D_mixture*psi_mixture(t))