Skip to content

Waves

Waves: strings, chains of springs, quantum wave packets, and abelian and non-abelian lattice gauge fields.

ChargedWaveFunctionElement(label, visual=None, *, u=0.0, w=0.0, D=0.5, q=1.0, A_x=0.0, A_t=0.0, E=0.0, duration=1.0)

Bases: Element

Wave function psi = u + i w of a charge q in a uniform electromagnetic potential.

The wave function follows (d_t - i q A_t) psi = i D (d_x - i q A_x)^2 psi. The vector potential A_x(t) = A_x - E min(t, duration) holds a uniform electric field E from time 0 to duration, and the scalar potential A_t is constant. Expanded, the equation is psi_t = i D psi_xx + 2 D q A_x psi_x + i (q A_t - D q^2 A_x^2) psi.

Parameters:

Name Type Description Default
label str

The label of the site or the field.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
u Annotated[float, StateSpec()]

The initial real part.

0.0
w Annotated[float, StateSpec()]

The initial imaginary part.

0.0
D Annotated[float, ParameterSpec(domain=open(0, oo))]

The coefficient of the Laplacian, ħ / (2 m).

0.5
q Annotated[float, ParameterSpec()]

The electric charge.

1.0
A_x Annotated[float, ParameterSpec()]

The vector potential at time 0.

0.0
A_t Annotated[float, ParameterSpec()]

The scalar potential.

0.0
E Annotated[float, ParameterSpec()]

The electric field.

0.0
duration Annotated[float, ParameterSpec(domain=open(0, oo))]

The time the electric field is on.

1.0

Examples:

>>> from skcomplex.systems.physics.waves import ChargedWaveFunctionElement
>>> ChargedWaveFunctionElement('site').q
1.0
Source code in src/skcomplex/systems/physics/waves/_charged.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    u: Annotated[float, StateSpec()] = 0.0,
    w: Annotated[float, StateSpec()] = 0.0,
    D: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.5,
    q: Annotated[float, ParameterSpec()] = 1.0,
    A_x: Annotated[float, ParameterSpec()] = 0.0,
    A_t: Annotated[float, ParameterSpec()] = 0.0,
    E: Annotated[float, ParameterSpec()] = 0.0,
    duration: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.u = u
    self.w = w
    self.D = D
    self.q = q
    self.A_x = A_x
    self.A_t = A_t
    self.E = E
    self.duration = duration

ColorLinkElement(label, visual=None, *, U=tuple(COLOR.identity))

Bases: Element

Link of a lattice with an SU(3) matrix U, the parallel transport of color along the link.

Parameters:

Name Type Description Default
label str

The label of the link or the group on the links.

required
visual VisualSpec | None

How the link is drawn, or None for default_visual_.

None
U Annotated[object, StateSpec(group=COLOR)]

The real components of the matrix of the link, the identity by default.

tuple(identity)

Attributes:

Name Type Description
default_visual_

The visual of the link when it sets none.

Examples:

>>> from skcomplex.systems.physics.waves import ColorLinkElement
>>> len(ColorLinkElement('link').U)
18
Source code in src/skcomplex/systems/physics/waves/_yang_mills.py
51
52
53
54
55
56
57
58
59
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    U: Annotated[object, StateSpec(group=COLOR)] = tuple(COLOR.identity),
) -> None:
    super().__init__(label, visual=visual)
    self.U = U

DrivenAnchorInteraction(label, *, target, k=1.0, center=0.0, amplitude=0.0, omega=1.0)

Bases: HarmonicPotentialInteraction

Spring that ties a particle to an anchor point, which oscillates transversally as amplitude sin(omega t).

The energy is k |x - center - (0, amplitude sin(omega t))|² / 2, and a zero amplitude gives a fixed anchor.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the particle.

required
k Annotated[float, ParameterSpec(domain=open(0, oo))]

The stiffness of the spring.

1.0
center Annotated[ArrayLike, ParameterSpec(vector=True)]

The rest position of the anchor.

0.0
amplitude Annotated[float, ParameterSpec()]

The amplitude of the transverse motion of the anchor.

0.0
omega Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The angular frequency of the anchor.

1.0

Examples:

>>> from skcomplex.systems.physics.waves import DrivenAnchorInteraction
>>> DrivenAnchorInteraction('end', target='mass_0', center=[0.0, 0.0], amplitude=0.1).amplitude
0.1
Source code in src/skcomplex/systems/physics/waves/_strings.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    k: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
    center: Annotated[npt.ArrayLike, ParameterSpec(vector=True)] = 0.0,
    amplitude: Annotated[float, ParameterSpec()] = 0.0,
    omega: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, target=target, k=k, center=center)
    self.amplitude = amplitude
    self.omega = omega

GaugeAnalysis(kind, space, variables=None, extents=(1, 1), charges=None, *, group=None)

Bases: BaseAnalysis

Gauge observables of a link state on the cell complex of a periodic lattice, one value per frame.

The kind 'wilson' is the mean cosine of the oriented sum of the link angles around the rectangles of given extents in every plane. The kind 'polyakov' is the mean cosine of the sum of the link angles along the last axis. The kind 'charge' is the sum over the plaquettes of a two-dimensional lattice of the oriented sums wrapped into one period around 0, over 2 pi. The kind 'gauss' is the largest absolute value of the codifferential of an electric link state minus a site charge.

An array of one dimension holds one snapshot, and an array of two dimensions one frame per row.

With a matrix group, the link states are the real components of the link matrices, and a loop is the path-ordered product of the link matrices around it. The kind 'wilson' is then the mean of Re tr W / N over the rectangles of every plane, and the kind 'polyakov' the modulus of the mean of tr P / N over the loops P that wind once along the last axis, the order parameter of deconfinement. An array of two dimensions then holds one snapshot, with the components of every link on the last axis, and an array of three dimensions one frame per row.

Parameters:

Name Type Description Default
kind str

'wilson', 'polyakov', 'charge', or 'gauss'.

required
space CellComplexSpace

The cell complex of the links.

required
variables str | None

The dotted name of the state of a trajectory, or None for a source that is an array.

None
extents tuple[int, int]

The sides of the rectangles of the kind 'wilson'.

(1, 1)
charges Sequence[float] | None

The charge of every site for the kind 'gauss', or None for none.

None
group MatrixGroupSpec | None

The matrix group of the links, or None for link angles.

None

Attributes:

Name Type Description
values_ Array

The link states, with one row per frame.

value_ Array

The observable of every frame.

Raises:

Type Description
AnalysisError

The kind is unknown, the space is not the cell complex of a periodic lattice, the charge is asked of a lattice that does not have two dimensions, or a kind other than a loop is asked of matrix links.

Examples:

>>> import numpy as np
>>> from skcomplex.core.components.spaces import CellComplexSpace, LatticeSpace
>>> from skcomplex.systems.physics.waves import GaugeAnalysis
>>> space = CellComplexSpace(LatticeSpace((4, 4))).prepare()
>>> GaugeAnalysis('wilson', space).prepare(np.zeros(32)).run().value_.tolist()
[1.0]
>>> from skcomplex.core.specs import MatrixGroupSpec
>>> group = MatrixGroupSpec('SU', 2)
>>> identity = np.tile(group.identity, (32, 1))
>>> GaugeAnalysis('polyakov', space, group=group).prepare(identity).run().value_.tolist()
[1.0]
Source code in src/skcomplex/systems/physics/waves/_yang_mills.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def __init__(
    self,
    kind: str,
    space: CellComplexSpace,
    variables: str | None = None,
    extents: tuple[int, int] = (1, 1),
    charges: Sequence[float] | None = None,
    *,
    group: MatrixGroupSpec | None = None,
) -> None:
    self.kind = kind
    self.space = space
    self.variables = variables
    self.extents = extents
    self.charges = charges
    self.group = group

GaugeLinkElement(label, visual=None, *, theta=0.0)

Bases: Element

Link of a lattice with an angle theta, the U(1) or Z_N variable of a lattice gauge theory.

Parameters:

Name Type Description Default
label str

The label of the link or the group on the links.

required
visual VisualSpec | None

How the link is drawn, or None for default_visual_.

None
theta Annotated[float, StateSpec(period=2 * pi)]

The angle of the link.

0.0

Attributes:

Name Type Description
default_visual_

The visual of the link when it sets none.

Examples:

>>> from skcomplex.systems.physics.waves import GaugeLinkElement
>>> GaugeLinkElement('link', theta=0.5).theta
0.5
Source code in src/skcomplex/systems/physics/waves/_gauge_fields.py
52
53
54
55
56
57
58
59
60
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    theta: Annotated[float, StateSpec(period=2 * pi)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.theta = theta

IsospinLinkElement(label, visual=None, *, U=tuple(ISOSPIN.identity))

Bases: Element

Link of a lattice with an SU(2) matrix U, the parallel transport of isospin along the link.

Parameters:

Name Type Description Default
label str

The label of the link or the group on the links.

required
visual VisualSpec | None

How the link is drawn, or None for default_visual_.

None
U Annotated[object, StateSpec(group=ISOSPIN)]

The real components of the matrix of the link, the identity by default.

tuple(identity)

Attributes:

Name Type Description
default_visual_

The visual of the link when it sets none.

Examples:

>>> from skcomplex.systems.physics.waves import IsospinLinkElement
>>> len(IsospinLinkElement('link').U)
8
Source code in src/skcomplex/systems/physics/waves/_yang_mills.py
88
89
90
91
92
93
94
95
96
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    U: Annotated[object, StateSpec(group=ISOSPIN)] = tuple(ISOSPIN.identity),
) -> None:
    super().__init__(label, visual=visual)
    self.U = U

LatticeHamiltonianInteraction(label, *, target, D=0.5)

Bases: Interaction

Hamiltonian -D laplacian + V of a particle on the sites of a lattice.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the particle, whose Hilbert space holds the operators laplacian and potential.

required
D Annotated[float, ParameterSpec(domain=open(0, oo))]

The coefficient of the Laplacian, ħ / (2 m).

0.5

Examples:

>>> from skcomplex.systems.physics.waves import LatticeHamiltonianInteraction
>>> LatticeHamiltonianInteraction('kinetic', target='packet').D
0.5
Source code in src/skcomplex/systems/physics/waves/_quantum.py
148
149
150
151
152
153
154
155
156
157
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    D: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.5,
) -> None:
    super().__init__(label)
    self.target = target
    self.D = D

MaxwellLinkElement(label, visual=None, *, A=0.0, A_d1=0.0)

Bases: Element

Link of a lattice with a vector potential A of second order, whose velocity A_d1 is the field -E.

Parameters:

Name Type Description Default
label str

The label of the link or the group on the links.

required
visual VisualSpec | None

How the link is drawn, or None for default_visual_.

None
A Annotated[float, StateSpec(order=2)]

The initial vector potential.

0.0
A_d1 float

The initial velocity of the vector potential, minus the electric field.

0.0

Attributes:

Name Type Description
default_visual_

The visual of the link when it sets none.

Examples:

>>> from skcomplex.systems.physics.waves import MaxwellLinkElement
>>> MaxwellLinkElement('link', A=0.5).A
0.5
Source code in src/skcomplex/systems/physics/waves/_gauge_fields.py
89
90
91
92
93
94
95
96
97
98
99
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    A: Annotated[float, StateSpec(order=2)] = 0.0,
    A_d1: float = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.A = A
    self.A_d1 = A_d1

PositionDephasingInteraction(label, *, target, gamma=0.1, n_sites=1)

Bases: Interaction

Dephasing of a particle in the position basis, one dissipator per site projector at the rate gamma.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the particle, whose Hilbert space holds the site projectors site_<k>.

required
gamma Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The dephasing rate.

0.1
n_sites int

The number of sites.

1

Examples:

>>> from skcomplex.systems.physics.waves import PositionDephasingInteraction
>>> PositionDephasingInteraction('dephasing', target='packet', gamma=0.5).gamma
0.5
Source code in src/skcomplex/systems/physics/waves/_quantum.py
196
197
198
199
200
201
202
203
204
205
206
207
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    gamma: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    n_sites: int = 1,
) -> None:
    super().__init__(label)
    self.target = target
    self.gamma = gamma
    self.n_sites = n_sites

StaticChargeElement(label, visual=None, *, q=0.0)

Bases: Element

Site of a lattice with a static electric charge q.

Parameters:

Name Type Description Default
label str

The label of the site or the group on the sites.

required
visual VisualSpec | None

How the site is drawn, or None for default_visual_.

None
q Annotated[float, ParameterSpec()]

The electric charge.

0.0

Attributes:

Name Type Description
default_visual_

The visual of the site when it sets none.

Examples:

>>> from skcomplex.systems.physics.waves import StaticChargeElement
>>> StaticChargeElement('site', q=-1.0).q
-1.0
Source code in src/skcomplex/systems/physics/waves/_gauge_fields.py
125
126
127
128
129
130
131
132
133
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    q: Annotated[float, ParameterSpec()] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.q = q

WaveFunctionElement(label, visual=None, *, u=0.0, w=0.0, D=0.5, V=0.0)

Bases: Element

Site of the wave function psi = u + i w of a particle with the potential V, by its real and imaginary parts.

The parts follow the Schrödinger equation i psi' = -D laplacian(psi) + V psi, so u' = -D laplacian(w) + V w and w' = D laplacian(u) - V u.

Parameters:

Name Type Description Default
label str

The label of the site.

required
visual VisualSpec | None

How the site is drawn, or None for none.

None
u Annotated[float, StateSpec()]

The initial real part.

0.0
w Annotated[float, StateSpec()]

The initial imaginary part.

0.0
D Annotated[float, ParameterSpec(domain=open(0, oo))]

The coefficient of the kinetic energy, hbar / (2 m).

0.5
V Annotated[float, ParameterSpec()]

The potential at the site.

0.0

Examples:

>>> from skcomplex.systems.physics.waves import WaveFunctionElement
>>> WaveFunctionElement('site', V=1.0).V
1.0
Source code in src/skcomplex/systems/physics/waves/_quantum.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    u: Annotated[float, StateSpec()] = 0.0,
    w: Annotated[float, StateSpec()] = 0.0,
    D: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.5,
    V: Annotated[float, ParameterSpec()] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.u = u
    self.w = w
    self.D = D
    self.V = V

WaveStringElement(label, visual=None, *, u=0.0, u_d1=0.0, c=1.0)

Bases: Element

Site of a string, with the transverse displacement u of order 2 and the wave speed c.

Parameters:

Name Type Description Default
label str

The label of the site.

required
visual VisualSpec | None

How the site is drawn, or None for none.

None
u Annotated[float, StateSpec(order=2)]

The initial displacement.

0.0
u_d1 float

The initial transverse velocity.

0.0
c Annotated[float, ParameterSpec(domain=open(0, oo))]

The wave speed.

1.0

Examples:

>>> from skcomplex.systems.physics.waves import WaveStringElement
>>> WaveStringElement('site', c=2.0).c
2.0
Source code in src/skcomplex/systems/physics/waves/_strings.py
51
52
53
54
55
56
57
58
59
60
61
62
63
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    u: Annotated[float, StateSpec(order=2)] = 0.0,
    u_d1: float = 0.0,
    c: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.u = u
    self.u_d1 = u_d1
    self.c = c

WilsonSamplingInteraction(label, *, target, beta=2.0, state='theta', move='rotate', values=(), width=1.5, rule='metropolis', schedule='colored', group=None, hits=1)

Bases: SamplingInteraction

Monte Carlo sampling of link variables with the local Wilson action of the plaquettes of a link.

For link angles the action is -beta sum_p cos(theta_p), where theta_p is the oriented sum of the link angles around the plaquette p. For the matrices of a group SU(N) it is -(beta / N) sum_p Re tr U_p, where U_p is the path-ordered product of the link matrices around p, and the move 'rotate' multiplies a link by a random matrix of the group near the identity of spread width.

Parameters:

Name Type Description Default
label str

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

required
target Annotated[str, RoleSpec()]

The path of the group on the edges of a cell complex.

required
beta Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The inverse coupling of the Wilson action.

2.0
state str

The name of the state that holds the link angles.

'theta'
move str

'rotate' for U(1) or a matrix group, or 'random-value' for Z_N.

'rotate'
values Sequence[float]

The angles a random-value move draws from.

()
width float

The largest change of a rotation.

1.5
rule str

The acceptance, 'metropolis', 'glauber', or 'heat-bath'.

'metropolis'
schedule str

'colored' or 'random-sequential'.

'colored'
group MatrixGroupSpec | None

The matrix group of the links, or None for link angles.

None
hits int

The number of moves proposed per link and sweep of matrix links.

1

Examples:

>>> from skcomplex.systems.physics.common.fields import WilsonSamplingInteraction
>>> WilsonSamplingInteraction('wilson', target='links', beta=2.0).beta
2.0
>>> from skcomplex.core.specs import MatrixGroupSpec
>>> color = WilsonSamplingInteraction('wilson', target='links', state='U', group=MatrixGroupSpec('SU', 3))
>>> color.objective.group.degree
3
Source code in src/skcomplex/systems/physics/common/fields/_wilson_sampling.py
 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
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    beta: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2.0,
    state: str = 'theta',
    move: str = 'rotate',
    values: Sequence[float] = (),
    width: float = 1.5,
    rule: str = 'metropolis',
    schedule: str = 'colored',
    group: MatrixGroupSpec | None = None,
    hits: int = 1,
) -> None:
    super().__init__(
        label,
        target=target,
        state=state,
        objective=self._get_action if group is None else HolonomyScoreSpec(self._get_trace_action, group, hits),
        move=move,
        rule=rule,
        schedule=schedule,
        values=values,
        width=width,
    )
    self.beta = beta
    self.group = group
    self.hits = hits

charged_schrodinger(n_sites=256, *, length=80.0, D=0.5, q=1.0, momentum=0.0, width=2.0, A_x=0.0, A_t=0.0, E=1.0, duration=2.0)

Return a Gaussian wave packet of a charge in a prescribed electromagnetic potential, after Schwichtenberg 4.3.

The wave function follows the Schrödinger equation with the covariant derivatives d - i q A, (d_t - i q A_t) psi = i D (d_x - i q A_x)^2 psi, on a periodic line with spectral derivatives. A mode of wave number k turns at the frequency D (k - q A_x)^2 - q A_t, so the packet moves at the group velocity 2 D (k - q A_x). A constant A_x is a pure gauge that only shifts the canonical momentum, while the electric field E switched on for duration changes A_x and accelerates the packet at the rate 2 D q E.

Parameters:

Name Type Description Default
n_sites int

The number of sites of the periodic lattice.

256
length float

The length of the periodic line, centered on 0.

80.0
D float

The coefficient of the Laplacian, ħ / (2 m).

0.5
q float

The electric charge.

1.0
momentum float

The mean wave number k of the packet.

0.0
width float

The width of the packet.

2.0
A_x float

The vector potential at time 0.

0.0
A_t float

The scalar potential.

0.0
E float

The electric field.

1.0
duration float

The time the electric field is on.

2.0

Returns:

Name Type Description
system System

The system, with the real part packet.u and the imaginary part packet.w of the wave function.

Examples:

>>> from skcomplex.systems.physics.waves import charged_schrodinger
>>> charged_schrodinger(64, E=0.5).elements[0].values['E']
0.5
Source code in src/skcomplex/systems/physics/waves/_charged.py
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
@register_system(
    field='physics/waves',
    tags=('quantum', 'gauge field', 'electromagnetism', 'wave packet', 'spectral', 'pde'),
    references=('Schwichtenberg 2020',),
    defaults={'until': 8.0, 'dt': 0.1},
)
def charged_schrodinger(
    n_sites: int = 256,
    *,
    length: float = 80.0,
    D: float = 0.5,
    q: float = 1.0,
    momentum: float = 0.0,
    width: float = 2.0,
    A_x: float = 0.0,
    A_t: float = 0.0,
    E: float = 1.0,
    duration: float = 2.0,
) -> System:
    """Return a Gaussian wave packet of a charge in a prescribed electromagnetic potential, after Schwichtenberg 4.3.

    The wave function follows the Schrödinger equation with the covariant derivatives `d - i q A`,
    `(d_t - i q A_t) psi = i D (d_x - i q A_x)^2 psi`, on a periodic line with spectral derivatives. A mode of wave
    number `k` turns at the frequency `D (k - q A_x)^2 - q A_t`, so the packet moves at the group velocity
    `2 D (k - q A_x)`. A constant `A_x` is a pure gauge that only shifts the canonical momentum, while the electric
    field `E` switched on for `duration` changes `A_x` and accelerates the packet at the rate `2 D q E`.

    Args:
        n_sites:
            The number of sites of the periodic lattice.

        length:
            The length of the periodic line, centered on 0.

        D:
            The coefficient of the Laplacian, `ħ / (2 m)`.

        q:
            The electric charge.

        momentum:
            The mean wave number `k` of the packet.

        width:
            The width of the packet.

        A_x:
            The vector potential at time 0.

        A_t:
            The scalar potential.

        E:
            The electric field.

        duration:
            The time the electric field is on.

    Returns:
        system:
            The system, with the real part `packet.u` and the imaginary part `packet.w` of the wave function.

    Examples:
        >>> from skcomplex.systems.physics.waves import charged_schrodinger
        >>> charged_schrodinger(64, E=0.5).elements[0].values['E']
        0.5
    """
    spacing = length / n_sites
    positions = spacing * np.arange(n_sites) - length / 2
    envelope = np.exp(-(positions**2) / (4 * width**2)) / (2 * np.pi * width**2) ** 0.25
    psi = envelope * np.exp(1j * momentum * positions)
    values = {
        'u': psi.real,
        'w': psi.imag,
        'D': D,
        'q': q,
        'A_x': A_x,
        'A_t': A_t,
        'E': E,
        'duration': duration,
    }
    space = LatticeSpace(n_sites, spacing=spacing)
    return System(
        elements=[
            FieldElement(
                'packet',
                element=ChargedWaveFunctionElement,
                space=space,
                values=values,
                discretization='spectral',
            ),
        ],
    )

lattice_gauge_fields(size=32, *, dimension=2, n=None, beta=2.0, dynamics='monte-carlo', charges=None, angles=None, amplitude=0.1, seed=0)

Return link variables on the cell complex of a periodic lattice, sampled or evolved in real time.

With Monte Carlo dynamics, link angles sample the Wilson action -beta sum_p cos(theta_p) under discrete time, as U(1) angles or as the angles 2 pi k / n of Z_n. With Hamiltonian dynamics, a potential on the links follows the lattice Maxwell equations A_t = -E and E_t = codifferential(dA) under continuous time, from static charges on the sites and a transverse wave. In two dimensions the U(1) mean plaquette at large size is I_1(beta) / I_0(beta).

Parameters:

Name Type Description Default
size int

The number of sites along every axis.

32
dimension int

The number of axes of the lattice.

2
n int | None

The order of the Z_n group, or None for U(1).

None
beta float

The inverse coupling of the Wilson action.

2.0
dynamics str

'monte-carlo' or 'hamiltonian'.

'monte-carlo'
charges Sequence[float] | None

The static charge of every site, of total 0, or None for none, with Hamiltonian dynamics.

None
angles Sequence[float] | None

The initial link angles, or None for random angles, with Monte Carlo dynamics.

None
amplitude float

The amplitude of the initial transverse wave, with Hamiltonian dynamics.

0.1
seed int

The seed of the initial link angles.

0

Returns:

Name Type Description
system System

The system.

Raises:

Type Description
ValidationError

The dynamics are unknown, or the charges do not hold one value per site of total 0.

Examples:

>>> from skcomplex.systems.physics.waves import lattice_gauge_fields
>>> [element.label for element in lattice_gauge_fields(4).elements]
['links']
>>> [element.label for element in lattice_gauge_fields(4, dynamics='hamiltonian').elements]
['links', 'charges']
Source code in src/skcomplex/systems/physics/waves/_gauge_fields.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
@register_system(
    field='physics/waves',
    tags=('gauge', 'lattice', 'monte carlo', 'maxwell', 'cell complex'),
    references=('Schwichtenberg 2020', 'Wilson 1974'),
    defaults={'until': 5000, 'dt': 1},
    settings=_gauge_settings,
    observables=_gauge_observables,
)
def lattice_gauge_fields(
    size: int = 32,
    *,
    dimension: int = 2,
    n: int | None = None,
    beta: float = 2.0,
    dynamics: str = 'monte-carlo',
    charges: Sequence[float] | None = None,
    angles: Sequence[float] | None = None,
    amplitude: float = 0.1,
    seed: int = 0,
) -> System:
    """Return link variables on the cell complex of a periodic lattice, sampled or evolved in real time.

    With Monte Carlo dynamics, link angles sample the Wilson action `-beta sum_p cos(theta_p)` under discrete time, as
    U(1) angles or as the angles `2 pi k / n` of Z_n. With Hamiltonian dynamics, a potential on the links follows the
    lattice Maxwell equations `A_t = -E` and `E_t = codifferential(dA)` under continuous time, from static charges on
    the sites and a transverse wave. In two dimensions the U(1) mean plaquette at large size is `I_1(beta) / I_0(beta)`.

    Args:
        size:
            The number of sites along every axis.

        dimension:
            The number of axes of the lattice.

        n:
            The order of the Z_n group, or `None` for U(1).

        beta:
            The inverse coupling of the Wilson action.

        dynamics:
            `'monte-carlo'` or `'hamiltonian'`.

        charges:
            The static charge of every site, of total 0, or `None` for none, with Hamiltonian dynamics.

        angles:
            The initial link angles, or `None` for random angles, with Monte Carlo dynamics.

        amplitude:
            The amplitude of the initial transverse wave, with Hamiltonian dynamics.

        seed:
            The seed of the initial link angles.

    Returns:
        system:
            The system.

    Raises:
        ValidationError:
            The dynamics are unknown, or the charges do not hold one value per site of total 0.

    Examples:
        >>> from skcomplex.systems.physics.waves import lattice_gauge_fields
        >>> [element.label for element in lattice_gauge_fields(4).elements]
        ['links']
        >>> [element.label for element in lattice_gauge_fields(4, dynamics='hamiltonian').elements]
        ['links', 'charges']
    """
    if dynamics not in DYNAMICS:
        error_msg = f'Unknown dynamics {dynamics!r}. Use one of {", ".join(DYNAMICS)}.'
        raise ValidationError(error_msg)
    space = CellComplexSpace(LatticeSpace((size,) * dimension)).prepare()
    if dynamics == 'hamiltonian':
        return _maxwell_system(space, charges, amplitude)
    return _monte_carlo_system(space, n, beta, angles, seed)

lattice_yang_mills(size=6, *, dimension=4, extent=None, colors=2, beta=2.3, width=0.5, hits=4, links=None, seed=0)

Return SU(2) or SU(3) matrices on the links of a periodic lattice, sampled by the Wilson action.

The link matrices sample the Wilson action -(beta / N) sum_p Re tr U_p under discrete time, where U_p is the ordered product of the link matrices around the plaquette p, with the inverse of a link traversed against its orientation. Every sweep proposes hits Metropolis moves per link, each a product with a random matrix of the group near the identity, and projects the links back onto the group. SU(2) is the isospin connection of the weak force, and SU(3) the color connection of the strong force, so that SU(3) is pure lattice QCD. The last axis has extent sites, the inverse temperature of the lattice, along which the Polyakov loop winds. In two dimensions the plaquettes are independent, and the SU(2) mean plaquette at large size is I_2(beta) / I_1(beta).

Parameters:

Name Type Description Default
size int

The number of sites along every axis but the last.

6
dimension int

The number of axes of the lattice.

4
extent int | None

The number of sites along the last axis, or None for size.

None
colors int

The degree N of the group SU(N), 2 or 3.

2
beta float

The inverse coupling of the Wilson action, 2 N / g^2.

2.3
width float

The spread around the identity of the matrices that move a link.

0.5
hits int

The number of moves proposed per link and sweep.

4
links ArrayLike | None

The initial link matrices, one N by N complex matrix per link, or None for matrices drawn from the Haar measure of the group.

None
seed int

The seed of the initial link matrices.

0

Returns:

Name Type Description
system System

The system.

Raises:

Type Description
ValidationError

The number of colors is not 2 or 3, or the initial links do not hold one matrix of the group per link.

Examples:

>>> from skcomplex.systems.physics.waves import lattice_yang_mills
>>> [element.label for element in lattice_yang_mills(2, dimension=2).elements]
['links']
>>> lattice_yang_mills(2, dimension=2, colors=3).elements[0].element.__name__
'ColorLinkElement'
Source code in src/skcomplex/systems/physics/waves/_yang_mills.py
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
@register_system(
    field='physics/waves',
    tags=('gauge', 'lattice', 'monte carlo', 'yang-mills', 'qcd', 'cell complex'),
    references=('Satz 2022', 'Schwichtenberg 2020', 'Wilson 1974', 'Creutz 1980'),
    defaults={'until': 100, 'dt': 1},
    observables=_yang_mills_observables,
)
def lattice_yang_mills(
    size: int = 6,
    *,
    dimension: int = 4,
    extent: int | None = None,
    colors: int = 2,
    beta: float = 2.3,
    width: float = 0.5,
    hits: int = 4,
    links: npt.ArrayLike | None = None,
    seed: int = 0,
) -> System:
    """Return SU(2) or SU(3) matrices on the links of a periodic lattice, sampled by the Wilson action.

    The link matrices sample the Wilson action `-(beta / N) sum_p Re tr U_p` under discrete time, where `U_p` is the
    ordered product of the link matrices around the plaquette `p`, with the inverse of a link traversed against its
    orientation. Every sweep proposes `hits` Metropolis moves per link, each a product with a random matrix of the group
    near the identity, and projects the links back onto the group. SU(2) is the isospin connection of the weak force,
    and SU(3) the color connection of the strong force, so that SU(3) is pure lattice QCD. The last axis has `extent`
    sites, the inverse temperature of the lattice, along which the Polyakov loop winds. In two dimensions the plaquettes
    are independent, and the SU(2) mean plaquette at large size is `I_2(beta) / I_1(beta)`.

    Args:
        size:
            The number of sites along every axis but the last.

        dimension:
            The number of axes of the lattice.

        extent:
            The number of sites along the last axis, or `None` for `size`.

        colors:
            The degree `N` of the group SU(N), 2 or 3.

        beta:
            The inverse coupling of the Wilson action, `2 N / g^2`.

        width:
            The spread around the identity of the matrices that move a link.

        hits:
            The number of moves proposed per link and sweep.

        links:
            The initial link matrices, one `N` by `N` complex matrix per link, or `None` for matrices drawn from the
            Haar measure of the group.

        seed:
            The seed of the initial link matrices.

    Returns:
        system:
            The system.

    Raises:
        ValidationError:
            The number of colors is not 2 or 3, or the initial links do not hold one matrix of the group per link.

    Examples:
        >>> from skcomplex.systems.physics.waves import lattice_yang_mills
        >>> [element.label for element in lattice_yang_mills(2, dimension=2).elements]
        ['links']
        >>> lattice_yang_mills(2, dimension=2, colors=3).elements[0].element.__name__
        'ColorLinkElement'
    """
    if colors not in LINKS:
        error_msg = f'The lattice gauge group is SU(2) or SU(3), so colors is 2 or 3, but got {colors!r}.'
        raise ValidationError(error_msg)
    element, group = LINKS[colors]
    shape = (size,) * (dimension - 1) + (size if extent is None else extent,)
    space = CellComplexSpace(LatticeSpace(shape)).prepare()
    count = space.incidences_[0].shape[0]
    if links is None:
        matrices = group.uniform_of(np.random.default_rng(seed), count)
    else:
        matrices = np.asarray(links, dtype=complex)
    if matrices.shape != (count, colors, colors) or not np.allclose(group.projection_of(matrices), matrices):
        error_msg = (
            f'The initial links are {matrices.shape[0]} matrices of the shape {matrices.shape[1:]}, but the lattice '
            f'takes {count} matrices of SU({colors}), one per link.'
        )
        raise ValidationError(error_msg)
    group_links = GroupElement(
        'links',
        element=element,
        space=space,
        on='edges',
        values={'U': group.components_of(matrices)},
    )
    sampling = WilsonSamplingInteraction(
        'wilson',
        target='links',
        beta=beta,
        state='U',
        width=width,
        group=group,
        hits=hits,
    )
    return System(elements=[group_links], interactions=[sampling], time=DiscreteTime())

schrodinger(n_sites=256, *, length=40.0, D=0.5, momentum=2.0, width=1.0, omega=0.0, formulation='fields', gamma=0.0)

Return a Gaussian wave packet that follows the Schrödinger equation, as its real and imaginary parts.

The wave function psi = u + i w follows psi_t = i D laplacian(psi) - i V psi, written as u_t = -D laplacian(w) + V w and w_t = D laplacian(u) - V u, on a periodic lattice with spectral derivatives. In units with ħ = 1, D = 1 / (2 m). The potential is the harmonic well V = omega² x² / (4 D), or zero for a free packet. A free packet of momentum k moves at the group velocity 2 D k and spreads, and its norm sum(u² + w²) is conserved.

The operator formulation builds the same particle as one quantum subsystem whose basis states are the sites, with the Hamiltonian -D laplacian + V of the same spectral Laplacian, normalised so that |psi|² is the probability of every site. It runs with the quantum solvers, and it accepts position dephasing at the rate gamma, which turns the state into a density matrix.

Parameters:

Name Type Description Default
n_sites int

The number of sites of the periodic lattice.

256
length float

The length of the periodic box, centered on 0.

40.0
D float

The coefficient of the Laplacian, ħ / (2 m).

0.5
momentum float

The mean wave number of the packet.

2.0
width float

The width of the packet.

1.0
omega float

The angular frequency of the harmonic well.

0.0
formulation str

'fields' for two real fields, or 'operators' for a particle on the sites of the lattice.

'fields'
gamma float

The rate of position dephasing of the operator formulation.

0.0

Returns:

Name Type Description
system System

The system, with the real part packet.u and the imaginary part packet.w of the wave function, or the quantum state schrodinger.state of the operator formulation.

Raises:

Type Description
ValidationError

The formulation is unknown, or the field formulation has a dephasing rate.

Examples:

>>> from skcomplex.systems.physics.waves import schrodinger
>>> schrodinger(64).elements[0].prepare().size_
64
Source code in src/skcomplex/systems/physics/waves/_quantum.py
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
@register_system(
    field='physics/waves',
    tags=('waves', 'field', 'pde', 'quantum', 'spectral'),
    references=('Schrödinger 1926', 'Schwichtenberg 2020'),
    defaults={'until': 4.0, 'dt': 0.1, 'solver': ETDRK4Solver(dt=0.01)},
)
def schrodinger(
    n_sites: int = 256,
    *,
    length: float = 40.0,
    D: float = 0.5,
    momentum: float = 2.0,
    width: float = 1.0,
    omega: float = 0.0,
    formulation: str = 'fields',
    gamma: float = 0.0,
) -> System:
    """Return a Gaussian wave packet that follows the Schrödinger equation, as its real and imaginary parts.

    The wave function `psi = u + i w` follows `psi_t = i D laplacian(psi) - i V psi`, written as `u_t = -D laplacian(w)
    + V w` and `w_t = D laplacian(u) - V u`, on a periodic lattice with spectral derivatives. In units with `ħ = 1`,
    `D = 1 / (2 m)`. The potential is the harmonic well `V = omega² x² / (4 D)`, or zero for a free packet. A free
    packet of momentum `k` moves at the group velocity `2 D k` and spreads, and its norm `sum(u² + w²)` is conserved.

    The operator formulation builds the same particle as one quantum subsystem whose basis states are the sites, with
    the Hamiltonian `-D laplacian + V` of the same spectral Laplacian, normalised so that `|psi|²` is the probability of
    every site. It runs with the quantum solvers, and it accepts position dephasing at the rate `gamma`, which turns
    the state into a density matrix.

    Args:
        n_sites:
            The number of sites of the periodic lattice.

        length:
            The length of the periodic box, centered on 0.

        D:
            The coefficient of the Laplacian, `ħ / (2 m)`.

        momentum:
            The mean wave number of the packet.

        width:
            The width of the packet.

        omega:
            The angular frequency of the harmonic well.

        formulation:
            `'fields'` for two real fields, or `'operators'` for a particle on the sites of the lattice.

        gamma:
            The rate of position dephasing of the operator formulation.

    Returns:
        system:
            The system, with the real part `packet.u` and the imaginary part `packet.w` of the wave function, or the
            quantum state `schrodinger.state` of the operator formulation.

    Raises:
        ValidationError:
            The formulation is unknown, or the field formulation has a dephasing rate.

    Examples:
        >>> from skcomplex.systems.physics.waves import schrodinger
        >>> schrodinger(64).elements[0].prepare().size_
        64
    """
    spacing = length / n_sites
    positions = spacing * np.arange(n_sites) - length / 2
    envelope = np.exp(-(positions**2) / (4 * width**2)) / (2 * np.pi * width**2) ** 0.25
    psi = envelope * np.exp(1j * momentum * positions)
    potential = omega**2 * positions**2 / (4 * D)
    space = LatticeSpace(n_sites, spacing=spacing)
    if formulation not in FORMULATIONS or (formulation == 'fields' and gamma):
        error_msg = (
            f'Unknown formulation {formulation!r}, or dephasing in the field formulation. Use one of '
            f'{", ".join(FORMULATIONS)}, and gamma > 0 only with the operator formulation.'
        )
        raise ValidationError(error_msg)
    if formulation == 'operators':
        amplitudes = psi / np.linalg.norm(psi)
        interactions: list[BaseInteraction] = [
            LatticeHamiltonianInteraction('kinetic', target='packet', D=D),
        ]
        if gamma > 0:
            interactions.append(
                PositionDephasingInteraction('dephasing', target='packet', gamma=gamma, n_sites=n_sites),
            )
        return System(
            'schrodinger',
            elements=[SiteElement('packet', amplitudes, hilbert=site_space(space, potential, projectors=gamma > 0))],
            interactions=interactions,
        )
    packet = FieldElement(
        'packet',
        element=WaveFunctionElement,
        space=space,
        values={'u': psi.real, 'w': psi.imag, 'D': D, 'V': potential},
        discretization='spectral',
    )
    return System(elements=[packet])

spring_chain(n_masses=15, *, length=1.0, k=1.0, mass=0.01, pluck=0.05, pluck_at=0.5, amplitude=0.0, omega=2.0)

Return masses joined in a row by springs of zero rest length between two anchors, a discrete string.

The masses start evenly spaced between the anchors at 0 and length, stretched, so the chain carries tension. With pluck, the masses start on a triangle of that height peaking at the fraction pluck_at of the length, the pluck of a guitar string. With amplitude, the left anchor moves transversally as amplitude sin(omega t) and sends a transverse wave along the chain. Springs of zero rest length are linear, so the transverse modes are independent, with frequencies 2 sqrt(k / m) sin(n pi / (2 (N + 1))).

Parameters:

Name Type Description Default
n_masses int

The number of masses between the anchors.

15
length float

The distance between the anchors.

1.0
k float

The stiffness of every spring.

1.0
mass float

The mass of every mass.

0.01
pluck float

The initial transverse displacement of the plucked point.

0.05
pluck_at float

The plucked point, as a fraction of the length.

0.5
amplitude float

The amplitude of the transverse motion of the left anchor.

0.0
omega float

The angular frequency of the left anchor.

2.0

Returns:

Name Type Description
system System

The system, with the masses mass_<i> and their positions mass_<i>.x in two dimensions.

Examples:

>>> from skcomplex.systems.physics.waves import spring_chain
>>> len(spring_chain(4).interactions)
5
Source code in src/skcomplex/systems/physics/waves/_strings.py
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
184
185
186
187
188
189
190
191
192
193
194
195
@register_system(
    field='physics/waves',
    tags=('waves', 'ode', 'springs', 'normal modes', 'mechanics'),
    references=('Argun et al. 2021',),
    defaults={'until': 20.0, 'dt': 0.05, 'solver': VelocityVerletSolver(dt=0.01)},
)
def spring_chain(
    n_masses: int = 15,
    *,
    length: float = 1.0,
    k: float = 1.0,
    mass: float = 0.01,
    pluck: float = 0.05,
    pluck_at: float = 0.5,
    amplitude: float = 0.0,
    omega: float = 2.0,
) -> System:
    """Return masses joined in a row by springs of zero rest length between two anchors, a discrete string.

    The masses start evenly spaced between the anchors at `0` and `length`, stretched, so the chain carries tension.
    With `pluck`, the masses start on a triangle of that height peaking at the fraction `pluck_at` of the length, the
    pluck of a guitar string. With `amplitude`, the left anchor moves transversally as `amplitude sin(omega t)` and
    sends a transverse wave along the chain. Springs of zero rest length are linear, so the transverse modes are
    independent, with frequencies `2 sqrt(k / m) sin(n pi / (2 (N + 1)))`.

    Args:
        n_masses:
            The number of masses between the anchors.

        length:
            The distance between the anchors.

        k:
            The stiffness of every spring.

        mass:
            The mass of every mass.

        pluck:
            The initial transverse displacement of the plucked point.

        pluck_at:
            The plucked point, as a fraction of the length.

        amplitude:
            The amplitude of the transverse motion of the left anchor.

        omega:
            The angular frequency of the left anchor.

    Returns:
        system:
            The system, with the masses `mass_<i>` and their positions `mass_<i>.x` in two dimensions.

    Examples:
        >>> from skcomplex.systems.physics.waves import spring_chain
        >>> len(spring_chain(4).interactions)
        5
    """
    spacing = length / (n_masses + 1)
    positions = spacing * np.arange(1, n_masses + 1)
    heights = _pluck(positions, length, pluck, pluck_at) if pluck else np.zeros(n_masses)
    masses = [
        MassPointElement(f'mass_{index}', x=[float(positions[index]), float(heights[index])], x_d1=[0.0, 0.0], m=mass)
        for index in range(n_masses)
    ]
    springs = [
        SpringInteraction(f'spring_{index}', first=f'mass_{index}', second=f'mass_{index + 1}', k=k)
        for index in range(n_masses - 1)
    ]
    anchors = [
        DrivenAnchorInteraction('left_end', target='mass_0', k=k, center=[0.0, 0.0], amplitude=amplitude, omega=omega),
        DrivenAnchorInteraction('right_end', target=f'mass_{n_masses - 1}', k=k, center=[length, 0.0]),
    ]
    return System(elements=masses, interactions=[*anchors, *springs], space=ContinuousSpace(n_dim=2))

standing_waves(n_sites=99, *, length=2.0, c=1.0, mode=1, amplitude=1.0)

Return a string with fixed ends on [-length / 2, length / 2] that vibrates as a standing wave.

The displacement follows the wave equation u'' = c² u_xx, written as u' = v and v' = c² laplacian(u) on a lattice whose edges hold the string at zero. The string starts at rest in the mode amplitude sin(mode pi (x + length / 2) / length), which oscillates in place with the period 2 length / (mode c), up to the discretization.

Parameters:

Name Type Description Default
n_sites int

The number of sites inside the string.

99
length float

The length of the string.

2.0
c float

The speed of the waves.

1.0
mode int

The number of half wavelengths along the string.

1
amplitude float

The initial amplitude.

1.0

Returns:

Name Type Description
system System

The system, with the displacement string.u and the velocity string.v.

Examples:

>>> from skcomplex.systems.physics.waves import standing_waves
>>> standing_waves(9).elements[0].prepare().size_
9
Source code in src/skcomplex/systems/physics/waves/_strings.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
@register_system(
    field='physics/waves',
    tags=('waves', 'field', 'pde', 'normal modes'),
    references=('Starkovich 2021',),
    defaults={'until': 4.0, 'dt': 0.05},
)
def standing_waves(
    n_sites: int = 99,
    *,
    length: float = 2.0,
    c: float = 1.0,
    mode: int = 1,
    amplitude: float = 1.0,
) -> System:
    """Return a string with fixed ends on `[-length / 2, length / 2]` that vibrates as a standing wave.

    The displacement follows the wave equation `u'' = c² u_xx`, written as `u' = v` and `v' = c² laplacian(u)` on a
    lattice whose edges hold the string at zero. The string starts at rest in the mode
    `amplitude sin(mode pi (x + length / 2) / length)`, which oscillates in place with the period
    `2 length / (mode c)`, up to the discretization.

    Args:
        n_sites:
            The number of sites inside the string.

        length:
            The length of the string.

        c:
            The speed of the waves.

        mode:
            The number of half wavelengths along the string.

        amplitude:
            The initial amplitude.

    Returns:
        system:
            The system, with the displacement `string.u` and the velocity `string.v`.

    Examples:
        >>> from skcomplex.systems.physics.waves import standing_waves
        >>> standing_waves(9).elements[0].prepare().size_
        9
    """
    spacing = length / (n_sites + 1)
    positions = spacing * np.arange(1, n_sites + 1)
    shape = amplitude * np.sin(mode * math.pi * positions / length)
    space = LatticeSpace(n_sites, spacing=spacing)
    edges = ('fixed', 0.0)
    string = FieldElement(
        'string',
        element=WaveStringElement,
        space=space,
        boundary={'u': edges},
        values={'u': shape, 'c': c},
    )
    tension = RateInteraction(
        'tension',
        target='string',
        state='u',
        rate=lambda s: s.target.c**2 * laplacian(s.target.u),
    )
    return System(elements=[string], interactions=[tension])