Skip to content

Pattern formation

Pattern formation: amplitude equations, rolls, phase ordering, and nonreciprocal conserved fields.

GinzburgLandauElement(label, visual=None, *, u=0.0, v=0.0, b=1.0, c=-1.5)

Bases: Element

Site of the complex Ginzburg-Landau equation A_t = A + (1 + i b) laplacian(A) - (1 - i c) |A|^2 A.

The amplitude A = u + i v is held as its real and imaginary parts. Without the Laplacian every site is a Stuart-Landau oscillator, and the dispersion b and the nonlinear frequency shift c set whether plane waves are stable or break into spiral defect chaos.

Parameters:

Name Type Description Default
label str

The label of the site or the field.

required
visual VisualSpec | None

How the site is drawn, or None for default_visual_.

None
u Annotated[float, StateSpec()]

The initial real part of the amplitude.

0.0
v Annotated[float, StateSpec()]

The initial imaginary part of the amplitude.

0.0
b Annotated[float, ParameterSpec()]

The linear dispersion.

1.0
c Annotated[float, ParameterSpec()]

The nonlinear frequency shift.

-1.5

Attributes:

Name Type Description
default_visual_

The visual of the site when it sets none.

Examples:

>>> from skcomplex.models.physics.pattern_formation import GinzburgLandauElement
>>> GinzburgLandauElement('site').c
-1.5
Source code in src/skcomplex/models/physics/pattern_formation/_ginzburg_landau.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    u: Annotated[float, StateSpec()] = 0.0,
    v: Annotated[float, StateSpec()] = 0.0,
    b: Annotated[float, ParameterSpec()] = 1.0,
    c: Annotated[float, ParameterSpec()] = -1.5,
) -> None:
    super().__init__(label, visual=visual)
    self.u = u
    self.v = v
    self.b = b
    self.c = c

NonreciprocalMixtureElement(label, visual=None, *, phi=0.0, psi=0.0, chi=0.0, alpha=0.3, D=0.1, kappa=1.0)

Bases: 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.

Parameters:

Name Type Description Default
label str

The label of the site or the field.

required
visual VisualSpec | None

How the site is drawn, or None for default_visual_.

None
phi Annotated[float, StateSpec()]

The initial demixing field.

0.0
psi Annotated[float, StateSpec()]

The initial diffusing field.

0.0
chi Annotated[float, ParameterSpec()]

The reciprocal coupling.

0.0
alpha Annotated[float, ParameterSpec()]

The nonreciprocal coupling.

0.3
D Annotated[float, ParameterSpec()]

The diffusion coefficient of psi.

0.1
kappa Annotated[float, ParameterSpec()]

The stiffness of the interfaces of phi.

1.0

Attributes:

Name Type Description
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
Source code in src/skcomplex/models/physics/pattern_formation/_conserved.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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

PatternFieldElement(label, visual=None, *, u=0.0)

Bases: Element

Site of a real field u whose patterns a free energy selects.

Parameters:

Name Type Description Default
label str

The label of the site or the field.

required
visual VisualSpec | None

How the site is drawn, or None for default_visual_.

None
u Annotated[float, StateSpec()]

The initial value of the field.

0.0

Attributes:

Name Type Description
default_visual_

The visual of the site when it sets none.

Examples:

>>> from skcomplex.models.physics.pattern_formation import PatternFieldElement
>>> PatternFieldElement('site', u=0.1).u
0.1
Source code in src/skcomplex/models/physics/pattern_formation/_pattern_field.py
35
36
37
def __init__(self, label: str, visual: VisualSpec | None = None, *, u: Annotated[float, StateSpec()] = 0.0) -> None:
    super().__init__(label, visual=visual)
    self.u = u

allen_cahn(shape=(128, 128), *, length=128.0, mean=0.0, kappa=1.0, random_state=0)

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.

Parameters:

Name Type Description Default
shape Sequence[int]

The number of sites along every axis of the periodic lattice.

(128, 128)
length float

The side of the domain.

128.0
mean float

The mean of the initial noise.

0.0
kappa float

The stiffness of the domain walls.

1.0
random_state int | Generator | None

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

0

Returns:

Name Type Description
system 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
Source code in src/skcomplex/models/physics/pattern_formation/_ordering.py
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@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,
    )

cahn_hilliard(shape=(64, 64), *, length=64.0, mean=0.0, kappa=1.0, temperature=0.0, random_state=0)

Return the Cahn-Hilliard equation, Model B, the conserved relaxation of an order parameter in a double well.

The order parameter follows phi_t = laplacian(-phi + phi^3 - kappa laplacian(phi)), a diffusion down the gradient of the chemical potential of the same free energy as the Allen-Cahn equation. Inside the spinodal, |mean| < 1 / sqrt(3), small noise decomposes into domains of phi near 1 and -1, which coarsen by diffusion through the bulk so that their size grows as t^(1/3), while the mean of phi stays fixed.

A positive temperature adds the thermal noise of Model B, div(sqrt(2 temperature) eta), which conserves the mean of phi as well. The field then uses finite differences, and an SDE solver such as EulerMaruyamaSolver integrates it, with a step below spacing^4 / (32 kappa).

Parameters:

Name Type Description Default
shape Sequence[int]

The number of sites along every axis of the periodic lattice.

(64, 64)
length float

The side of the domain.

64.0
mean float

The mean of the order parameter, which the dynamics conserve.

0.0
kappa float

The stiffness of the interfaces.

1.0
temperature float

The temperature of the conserved thermal noise, or 0 for the deterministic equation.

0.0
random_state int | Generator | None

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

0

Returns:

Name Type Description
system System

The model, with the field mixture.phi.

Examples:

>>> from skcomplex.models.physics.pattern_formation import cahn_hilliard
>>> cahn_hilliard((8, 8), mean=0.3).interactions[0].conserved
True
>>> [interaction.label for interaction in cahn_hilliard((8, 8), temperature=0.1).interactions]
['energy', 'noise']
Source code in src/skcomplex/models/physics/pattern_formation/_ordering.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
@register_model(
    field='physics/pattern_formation',
    tags=('field', 'phase separation', 'coarsening', 'conserved', 'model b', 'spectral', 'pde', 'conserved noise'),
    references=REFERENCES,
    defaults={'until': 1000.0, 'dt': 10.0, 'solver': ETDRK4Solver(dt=0.25)},
)
def cahn_hilliard(
    shape: Sequence[int] = (64, 64),
    *,
    length: float = 64.0,
    mean: float = 0.0,
    kappa: float = 1.0,
    temperature: float = 0.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Cahn-Hilliard equation, Model B, the conserved relaxation of an order parameter in a double well.

    The order parameter follows `phi_t = laplacian(-phi + phi^3 - kappa laplacian(phi))`, a diffusion down the
    gradient of the chemical potential of the same free energy as the Allen-Cahn equation. Inside the spinodal,
    `|mean| < 1 / sqrt(3)`, small noise decomposes into domains of `phi` near 1 and -1, which coarsen by diffusion
    through the bulk so that their size grows as `t^(1/3)`, while the mean of `phi` stays fixed.

    A positive temperature adds the thermal noise of Model B, `div(sqrt(2 temperature) eta)`, which conserves the mean
    of `phi` as well. The field then uses finite differences, and an SDE solver such as `EulerMaruyamaSolver` integrates
    it, with a step below `spacing^4 / (32 kappa)`.

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

        length:
            The side of the domain.

        mean:
            The mean of the order parameter, which the dynamics conserve.

        kappa:
            The stiffness of the interfaces.

        temperature:
            The temperature of the conserved thermal noise, or 0 for the deterministic equation.

        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 cahn_hilliard
        >>> cahn_hilliard((8, 8), mean=0.3).interactions[0].conserved
        True
        >>> [interaction.label for interaction in cahn_hilliard((8, 8), temperature=0.1).interactions]
        ['energy', 'noise']
    """
    return _ordering(
        shape=shape,
        length=length,
        mean=mean,
        kappa=kappa,
        random_state=random_state,
        conserved=True,
        temperature=temperature,
    )

complex_ginzburg_landau(shape=(64, 64), *, length=64.0, b=1.0, c=-1.5, random_state=0)

Return the complex Ginzburg-Landau equation A_t = A + (1 + i b) laplacian(A) - (1 + i c) |A|^2 A.

The amplitude A = u + i v is held as its real and imaginary parts, and the linear part couples them per Fourier mode. With 1 + b c < 0 the uniform oscillation is unstable and spiral defects turn into turbulence.

Parameters:

Name Type Description Default
shape Sequence[int]

The number of sites along both axes of the periodic lattice.

(64, 64)
length float

The side of the square.

64.0
b float

The linear dispersion.

1.0
c float

The nonlinear dispersion.

-1.5
random_state int | Generator | None

The seed or generator of the small random initial amplitude.

0

Returns:

Name Type Description
system System

The model, with the real part medium.u and the imaginary part medium.v.

Examples:

>>> from skcomplex.models.physics.pattern_formation import complex_ginzburg_landau
>>> complex_ginzburg_landau((8, 8)).elements[0].values['c']
-1.5
Source code in src/skcomplex/models/physics/pattern_formation/_patterns.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@register_model(
    field='physics/pattern_formation',
    tags=('field', 'chaos', 'spirals', 'spectral', 'pde'),
    references=('Aranson and Kramer 2002', 'Gros 2015'),
    defaults={'until': 50.0, 'dt': 1.0, 'solver': ETDRK4Solver(dt=0.1)},
)
def complex_ginzburg_landau(
    shape: Sequence[int] = (64, 64),
    *,
    length: float = 64.0,
    b: float = 1.0,
    c: float = -1.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the complex Ginzburg-Landau equation `A_t = A + (1 + i b) laplacian(A) - (1 + i c) |A|^2 A`.

    The amplitude `A = u + i v` is held as its real and imaginary parts, and the linear part couples them per Fourier
    mode. With `1 + b c < 0` the uniform oscillation is unstable and spiral defects turn into turbulence.

    Args:
        shape:
            The number of sites along both axes of the periodic lattice.

        length:
            The side of the square.

        b:
            The linear dispersion.

        c:
            The nonlinear dispersion.

        random_state:
            The seed or generator of the small random initial amplitude.

    Returns:
        system:
            The model, with the real part `medium.u` and the imaginary part `medium.v`.

    Examples:
        >>> from skcomplex.models.physics.pattern_formation import complex_ginzburg_landau
        >>> complex_ginzburg_landau((8, 8)).elements[0].values['c']
        -1.5
    """
    shape = tuple(shape)
    rng = np.random.default_rng(random_state)
    values = {'u': _noise(shape, 0.01, rng), 'v': _noise(shape, 0.01, rng), 'b': b, 'c': c}
    space = LatticeSpace(shape, spacing=length / shape[0])
    return System(
        elements=[
            FieldElement(
                'medium',
                element=GinzburgLandauElement,
                space=space,
                values=values,
                discretization='spectral',
            ),
        ],
    )

nonreciprocal_cahn_hilliard(shape=(128,), *, length=40.0, chi=0.0, alpha=0.3, D=0.1, kappa=1.0, random_state=0)

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.

Parameters:

Name Type Description Default
shape Sequence[int]

The number of sites along every axis of the periodic lattice.

(128,)
length float

The side of the domain.

40.0
chi float

The reciprocal part of the cross-diffusion.

0.0
alpha float

The nonreciprocal part of the cross-diffusion.

0.3
D float

The diffusivity of psi.

0.1
kappa float

The stiffness of the interfaces of phi.

1.0
random_state int | Generator | None

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

0

Returns:

Name Type Description
system 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
Source code in src/skcomplex/models/physics/pattern_formation/_conserved.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
@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)])

swift_hohenberg(shape=(64, 64), *, length=16 * math.pi, r=0.3, random_state=0)

Return the Swift-Hohenberg equation u_t = r u - (1 + laplacian)^2 u - u^3, which forms rolls of length 2 pi.

Above onset, r > 0, small noise grows into rolls whose wavenumber is near 1, the fastest growing one.

Parameters:

Name Type Description Default
shape Sequence[int]

The number of sites along every axis of the periodic lattice.

(64, 64)
length float

The side of the domain.

16 * pi
r float

The distance above onset.

0.3
random_state int | Generator | None

The seed or generator of the noise.

0

Returns:

Name Type Description
system System

The model, with the field cells.u.

Examples:

>>> from skcomplex.models.physics.pattern_formation import swift_hohenberg
>>> swift_hohenberg((8, 8), r=0.1).interactions[0].r
0.1
Source code in src/skcomplex/models/physics/pattern_formation/_patterns.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
@register_model(
    field='physics/pattern_formation',
    tags=('field', 'rolls', 'convection', 'spectral', 'pde'),
    references=('Swift and Hohenberg 1977', 'Cross and Hohenberg 1993', 'Datseris and Parlitz 2022'),
    defaults={'until': 200.0, 'dt': 5.0, 'solver': ETDRK4Solver(dt=0.5)},
)
def swift_hohenberg(
    shape: Sequence[int] = (64, 64),
    *,
    length: float = 16 * math.pi,
    r: float = 0.3,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Swift-Hohenberg equation `u_t = r u - (1 + laplacian)^2 u - u^3`, which forms rolls of length 2 pi.

    Above onset, `r > 0`, small noise grows into rolls whose wavenumber is near 1, the fastest growing one.

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

        length:
            The side of the domain.

        r:
            The distance above onset.

        random_state:
            The seed or generator of the noise.

    Returns:
        system:
            The model, with the field `cells.u`.

    Examples:
        >>> from skcomplex.models.physics.pattern_formation import swift_hohenberg
        >>> swift_hohenberg((8, 8), r=0.1).interactions[0].r
        0.1
    """
    shape = tuple(shape)
    values = {'u': _noise(shape, 0.01, random_state)}
    space = LatticeSpace(shape, spacing=length / shape[0])
    return System(
        elements=[
            FieldElement('cells', element=PatternFieldElement, space=space, values=values, discretization='spectral'),
        ],
        interactions=[SwiftHohenbergEnergyInteraction('energy', target='cells', r=r)],
    )