Skip to content

Allen-Cahn non-conserved relaxation (Model A)

The Allen-Cahn equation, Model A, the non-conserved relaxation of an order parameter in a double well.

  • Name: allen_cahn
  • Field: physics/pattern_formation
  • Formalism: ordinary differential equations
  • References: Brauns and Marchetti 2024; Allen and Cahn 1979; Cahn and Hilliard 1958; Bray 1994

Factory

@register_model(
    field='physics/pattern_formation',
    tags=('field', 'phase ordering', 'coarsening', 'model a', 'spectral', 'pde'),
    references=REFERENCES,
    defaults={'until': 100.0, 'dt': 1.0, 'solver': ETDRK4Solver(dt=0.5)},
)
def allen_cahn(
    shape: Sequence[int] = (128, 128),
    *,
    length: float = 128.0,
    mean: float = 0.0,
    kappa: float = 1.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Allen-Cahn equation, Model A, the non-conserved relaxation of an order parameter in a double well.

    The order parameter follows `phi_t = phi - phi^3 + kappa laplacian(phi)`, the gradient descent of the free energy
    density `-phi^2 / 2 + phi^4 / 4 + kappa |grad(phi)|^2 / 2`. From small noise, domains of `phi = 1` and `phi = -1`
    form and coarsen as their walls move by curvature, so their size grows as `t^(1/2)`. The mean of `phi` is not
    conserved, and a domain shrinks away instead of exchanging material with its neighbours.

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

        length:
            The side of the domain.

        mean:
            The mean of the initial noise.

        kappa:
            The stiffness of the domain walls.

        random_state:
            The seed or generator of the initial noise of amplitude 0.05.

    Returns:
        system:
            The model, with the field `mixture.phi`.

    Examples:
        >>> from skcomplex.models.physics.pattern_formation import allen_cahn
        >>> allen_cahn((8, 8), kappa=2.0).interactions[0].kappa
        2.0
    """
    return _ordering(
        shape=shape,
        length=length,
        mean=mean,
        kappa=kappa,
        random_state=random_state,
        conserved=False,
    )

Components

class PhaseFieldElement(Element):
    """Site of a phase field, which holds the order parameter `phi` that free energies and noise move.

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

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

        phi:
            The initial order parameter.

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

    Examples:
        >>> from skcomplex.models.physics.common.fields import PhaseFieldElement
        >>> PhaseFieldElement('site', phi=0.5).phi
        0.5
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        phi: Annotated[float, StateSpec()] = 0.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.phi = phi

class LandauEnergyInteraction(GradientInteraction):
    """Gradient flow of a field `phi` in the Landau free energy of a double well with a cost of gradients.

    The free energy is `F = integral -phi^2 / 2 + phi^4 / 4 + kappa |grad phi|^2 / 2`. Its functional derivative is
    the chemical potential `mu = -phi + phi^3 - kappa laplacian(phi)`. A non-conserved field relaxes as `phi_t = -mu`,
    the Allen-Cahn equation of model A, and a conserved field as `phi_t = laplacian(mu)`, the Cahn-Hilliard equation of
    model B, whose total stays constant.

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

        target:
            The path of the field.

        state:
            The name of the state of the field that holds the order parameter.

        kappa:
            The cost of gradients, the square of the width of the interfaces.

        conserved:
            Whether the order parameter is conserved.

    Examples:
        >>> from types import SimpleNamespace
        >>> from sage.all__sagemath_symbolics import var
        >>> from skcomplex.models.physics.common.fields import LandauEnergyInteraction
        >>> energy = LandauEnergyInteraction('energy', target='mixture').check().prepare()
        >>> namespace = SimpleNamespace(target=SimpleNamespace(phi=var('phi')), kappa=var('kappa'))
        >>> [law.value for law in energy.run(namespace).laws_]
        [-phi^3 + kappa*laplacian(phi) + phi]
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        state: str = 'phi',
        kappa: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        conserved: bool = False,
    ) -> None:
        super().__init__(label, target=target, states=state, variational=True, conserved=conserved)
        self.state = state
        self.kappa = kappa

    def _get_potential(self, namespace: SimpleNamespace) -> Expression:
        """Return the density `-phi^2 / 2 + phi^4 / 4 - kappa phi laplacian(phi) / 2`, the gradient term by parts.

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

        Returns:
            density:
                The density of the free energy.
        """
        phi = getattr(namespace.target, self.state)
        return -(phi**2) / 2 + phi**4 / 4 - namespace.kappa * phi * laplacian(phi) / 2

Equations

mixture.phi[i]: diff(phi_mixture(t), t) == -phi_mixture(t)^3 + kappa_energy*laplacian(phi_mixture(t)) + phi_mixture(t)