Differential Forms

Fields live on the cells of a Lattice. A 0-form assigns a value to every site, a 1-form to every link, a 2-form to every plaquette, and so on; a p-form in D dimensions has \(\binom{D}{p}\) components per site.

class supervillain.lattice.Form(input_array, *, degree, lattice, dtype=None)[source]

Bases: ndarray

A differential p-form on a Lattice.

In the interlaced \((2N)^D\) array a p-form component with odd directions \(I\) lives at interlaced coordinates \(\xi\) with

\[\begin{split}\xi_k = \begin{cases} 2 x_k & k \notin I \\ 2 x_k + 1 & k \in I \end{cases}\end{split}\]

so the even directions are site directions and the odd directions are the directions the cell spans. In both cases \(\lfloor \xi_k / 2 \rfloor = x_k\): the physical site is always the floor of the interlaced coordinate divided by 2.

Actually storing data in this interlaced layout is very wasteful—for a \(p\)-form only \(\binom{D}{p}/2^D\) of the array elements are used. This introduces a nontrivial memory and speed overhead. Instead, we store the data in a dense format in an array of shape \(\left(\binom{D}{p}, N, N, ..., N\right)\).

The 0th axis indexes the \(\binom{D}{p}\) components, listed in lexicographic order by the sorted tuple of directions that are “form directions”. The remaining axes index the physical lattice site itself, \(x\).

A Form is a subclass of numpy.ndarray, so it supports all the usual numpy operations. Element-wise operations (±, *, /, unary , abs, **, np.sqrt, np.isclose, ==, and so on) return a Form of the same degree when all Form operands share a degree. Mixed-degree arithmetic is left as a plain ndarray because the degree of the result would be ambiguous, but the wedge product is defined below.

Reductions (sum, max, min, and so on) return plain numpy scalars or arrays.

classmethod spatial_shape(*, degree, lattice)[source]
Returns

Shape of a form with degree degree on lattice with \(D\) dimensions and \(N\) sites, (C(D,p), N, N, ..., N).

Return type

tuple

component(*dirs)[source]

View of a single component’s spatial data, shape (N,…,N).

Parameters

*dirs (int or tuple of int) – Direction indices, either as separate arguments f.component(0, 2) or as a single tuple f.component((0, 2)).

Returns

A view of shape (N,...,N); writes back to the Form.

Return type

np.ndarray

to_interlaced()[source]

Embed the compact form into a \((2N)^D\) interlaced array.

Component \(I\) occupies the sub-array where direction \(k\) uses odd indices if \(k \in I\) and even indices otherwise; every non-p-form site is zero.

Inverse of from_interlaced().

Returns

Plain array of shape (2N, 2N, ..., 2N).

Return type

np.ndarray

classmethod from_interlaced(p, data, lattice=None)[source]

Construct a dense Form from an interlaced \((2N)^D\) array.

Inverse of to_interlaced().

Parameters
  • p (int) – Form degree.

  • data (np.ndarray) – Interlaced array of shape (2N, 2N, ..., 2N); only sites with exactly p odd coordinates are read.

  • lattice (Lattice, optional) – Inferred from data.shape if omitted.

Returns

The compact p-form whose interlaced embedding reproduces data at p-form sites.

Return type

Form

face_sum()[source]

Sum this p-form onto its (p-1)-faces, returning a (p-1)-form.

For each (p-1)-form output component \(M\) at site \(x\):

\[g_M[x] = \sum_{O \supset M} \big( f_O[x] + f_O[x - \hat{e}_e] \big)\]

where the sum runs over p-cells \(O = M \cup \{e\}\).

Returns

The (p-1)-form face sum, or 0 if this is a 0-form.

Return type

Form

coface_sum()[source]

Sum this p-form onto incident (p+1)-cofaces, returning a (p+1)-form.

For each (p+1)-form output component \(O\) at site \(x\):

\[g_O[x] = \sum_{M \subset O} \big( f_M[x] + f_M[x + \hat{e}_{o_j}] \big)\]

where the sum runs over p-faces \(M = O \setminus \{o_j\}\) of \(O\). Dual to face_sum(); unlike d(), all contributions enter unsigned.

Returns

The (p+1)-form coface sum, or 0 if this is a D-form.

Return type

Form

Translation

Forms can be translated around the (periodic) lattice.

supervillain.lattice.pull(form, shift)[source]

Translation operator \(T_{\Delta x}\): pull content from position \(x + \Delta x\) to \(x\).

\[T_{\Delta x} f[\ldots, x] = \texttt{pull}(f, \Delta x)[\ldots, x] = f[\ldots, x + \Delta x] \quad \text{(periodic)}\]
Parameters
  • form (np.ndarray) – Array whose last len(shift) axes are the spatial directions.

  • shift (sequence of int) – One integer per spatial direction.

Return type

np.ndarray

supervillain.lattice.push(form, shift)[source]

Translate the form forward by \(\Delta x\).

\[\texttt{push}(f, \Delta x)[\ldots, x] = f[\ldots, x - \Delta x] \quad \text{(periodic)}\]
Parameters
  • form (np.ndarray) – Array whose last len(shift) axes are the spatial directions.

  • shift (sequence of int) – One integer per spatial direction.

Return type

np.ndarray

Sign Conventions

A component of a \(p\)-form is labeled by a strictly increasing tuple of \(p\) directions, stored along axis 0 (see The Interlaced Picture): the component \(f_I\) with \(I = (i_1 < \cdots < i_p)\) is the coefficient of \(dx_{i_1} \wedge \cdots \wedge dx_{i_p}\), with the factors in increasing order.

Every sign in the exterior calculus has the same origin: an operation naturally produces a tuple of directions out of order, and restoring sorted order costs the signature of the sorting permutation. Write \(\sigma(t)\) for the sign of the permutation that sorts the tuple \(t\), and \(\frown\) for concatenation. Write \(\Delta_e A[x] = A[x + \hat{e}_e] - A[x]\) for the forward finite difference in direction \(e\), and \(\nabla^*_e A[x] = A[x] - A[x - \hat{e}_e]\) for the backward finite difference.

Exterior Derivative

\[(d f)_{O}[x] = \sum_{e \in O} \sigma\big((e) \frown (O \setminus e)\big)\; \Delta_e f_{O \setminus e}[x]\]

Adding direction \(e\) wedges \(dx_e\) onto the front of \(dx_{O \setminus e}\); sorting it into place costs \(\sigma\big((e) \frown (O \setminus e)\big) = (-1)^{\#\{i \in O \setminus e \;:\; i < e\}}\).

supervillain.lattice.d(f)[source]

The exterior derivative of a p-form, a (p+1)-form.

For each output component \(O = (o_0, \ldots, o_p)\):

\[(df)_O[x] = \sum_{j=0}^{p} (-1)^j \, \Delta_{o_j} f_{O \setminus \{o_j\}}[x]\]

where \(\Delta_k A[x] = A[x + \hat{e}_k] - A[x]\) is the forward finite difference. The sign \((-1)^j\) is the signature of the permutation sorting \(o_j\) into the remaining directions (see Sign Conventions).

Parameters

f (Form) – A p-form on a Lattice.

Returns

The (p+1)-form \(df\), or the scalar 0 if \(f\) is a D-form.

Return type

Form

The exterior derivative is exact, meaning that it satisfies

\[d^2 = d \circ d = 0,\]

which is satisfied by supervillain.lattice.d(). This identity is tested by test_d_nilpotent in test/test_lattice.py.

Codifferential

The codifferential is defined as the formal adjoint of the exterior derivative,

(1)\[\langle d a, b \rangle = \langle a, \delta b \rangle\]

where the \(\langle \cdot, \cdot \rangle\) is inner product. This identity is tested by the test_compact_adjointness test in test/test_lattice.py.

\[(\delta f)_{M}[x] = - \sum_{e \notin M} \sigma\big((e) \frown M\big)\; \nabla^*_e f_{M \cup \{e\}}[x]\]

The same insertion sign accounts for removing \(e\) from the sorted source tuple \(M \cup \{e\}\); the overall minus makes \(\delta\) the formal adjoint of \(d\).

supervillain.lattice.delta(f)[source]

Codifferential (formal adjoint of d) of a p-form, returning a (p-1)-form.

For each output component \(M = (m_0, \ldots, m_{p-1})\) and each direction \(e \notin M\), let \(j = \#\{m \in M : m < e\}\) (the position where \(e\) would be inserted to keep \(M \cup \{e\}\) sorted):

\[(\delta f)_M[x] = - \sum_{e \notin M} (-1)^j \, \nabla^*_e f_{M \cup \{e\}}[x]\]

where \(\nabla^*_e A[x] = A[x] - A[x - \hat{e}_e]\) is the backward finite difference. With the overall minus, \(\delta\) is the formal adjoint of d() under the componentwise inner product ((1)).

Parameters

f (Form) – A p-form on a Lattice.

Returns

The (p-1)-form \(\delta f\), or the scalar 0 if \(f\) is a 0-form.

Return type

Form

The codifferential is nilpotent, meaning that it satisfies

\[\delta^2 = \delta \circ \delta = 0.\]

This identity is tested by the test_codifferential_nilpotent test in test/test_lattice.py.

The continuum identity \(\delta = (-1)^{D(k+1)+1}\,\star\,d\,\star\) holds on the lattice only up to a translation; see the note below.

Laplacian

The Hodge–de Rham Laplacian is the symmetric combination of the exterior derivative and the codifferential,

\[\Delta = d\delta + \delta d,\]

mapping a \(p\)-form to a \(p\)-form. Because \(\delta\) is the adjoint of \(d\) ((1)), the Laplacian is self-adjoint and positive semidefinite,

\[\langle \Delta f, f \rangle = \langle d f, d f \rangle + \langle \delta f, \delta f \rangle \geq 0,\]

checked by the test_laplacian_self_adjoint and test_laplacian_positive_semidefinite tests in test/test_lattice.py.

On the flat periodic lattice \(d\) and \(\delta\) are constant-coefficient combinations of the commuting shift operators \(T_k\), so the Weitzenböck cross-terms cancel through \(\{dx_k \wedge,\, \iota_l\} = \delta_{kl}\) and the Laplacian acts diagonally on each component \(I\) as the negative of the ordinary nearest-neighbor scalar Laplacian,

\[(\Delta f)_{I}[x] = \sum_{k=0}^{D-1}\big(2 f_I[x] - f_I[x + \hat{e}_k] - f_I[x - \hat{e}_k]\big),\]

with no mixing between the \(\binom{D}{p}\) components. This diagonal form agrees with the explicit composition \(d\delta + \delta d\), as checked by the test_laplacian_matches_d_delta test in test/test_lattice.py.

supervillain.lattice.laplacian(f)[source]

Hodge–de Rham Laplacian of a p-form, a p-form of the same degree.

The Laplacian (or Laplace–de Rham operator) is

\[\Delta = d\delta + \delta d,\]

the symmetric combination of the exterior derivative and its formal adjoint the codifferential. Because delta() is the adjoint of d(), the Laplacian is self-adjoint and positive semidefinite under the componentwise inner product,

\[\langle \Delta f, f \rangle = \langle d f, d f \rangle + \langle \delta f, \delta f \rangle \geq 0.\]
Parameters

f (Form) – A p-form on a Lattice.

Returns

The p-form \(\Delta f = (d\delta + \delta d) f\).

Return type

Form

Hodge Star

For each output component \(J\) (a sorted \((D-p)\)-tuple of directions), let \(I = \{0,\ldots,D-1\} \setminus J\) be its complement:

\[(\star f)_{J}[x] = \sigma\big(I \frown J\big)\; f_{I}\!\left[x - {\textstyle\sum_{k \in I}} \hat{e}_k\right]\]

The sign sorts the concatenation \((I, J)\) into \((0, \ldots, D-1)\), so that \(dx_I \wedge [\sigma(I \frown J)\, dx_J] = dx_0 \wedge \cdots \wedge dx_{D-1}\); the shift \(-\hat{e}_I\) aligns the dual cell with the original in the interlaced geometry.

supervillain.lattice.star(f)[source]

Hodge star of a p-form, a (D-p)-form.

For each output component \(J\) (a sorted \((D-p)\)-tuple of directions), let \(I\) be the complement of \(J\) in \(\{0, \ldots, D-1\}\):

\[(\star f)_J[x] = \sigma(I \frown J) \; f_I[x - \hat{e}_I]\]

where \(\sigma(I \frown J) = (-1)^{\#\{(i, j) \in I \times J \,:\, i > j\}}\) is the sign of the permutation sorting the concatenation \((I \frown J)\) and \(\hat{e}_I = \sum_{k \in I} \hat{e}_k\).

In the discrete interlaced geometry, a p-form and its Hodge dual are centered at different lattice positions. The shift aligns them so that the inner-product identity holds after summing over the lattice:

\[\sum_{x, I} a_I[x] \, b_I[x] = \sum_x (a \wedge \star b)_{(0, \ldots, D-1)}[x]\]

For \(p = 0\) and \(p = D\) the shift is trivial (\(\hat{e}_I = 0\) or the shifts cancel in the wedge).

Parameters

f (Form) – A p-form on a Lattice.

Returns

The (D-p)-form \(\star f\).

Return type

Form

The Hodge inner-product identity

\[\sum_{x} (a \wedge \star b)_{(0, \ldots, D-1)}[x] = \langle a, b \rangle\]

holds exactly, which is checked by the test_hodge_inner_product test in test/test_lattice.py.

Wedge Product

On the lattice the wedge product sums over all shuffles \(O = A \sqcup B\), where \(A\) are the \(n\) directions of \(a\) and \(B\) are the \(m\) directions of \(b\):

\[(a \wedge b)_{O}[x] = \sum_{O = A \sqcup B} \sigma\big(A \frown B\big)\; a_A[x]\; b_B\!\left[x + {\textstyle\sum_{k \in A}} \hat{e}_k\right]\]

Each shuffle contributes the sign that sorts \((A, B)\) back into \(O\), and \(b\) is evaluated on the far side of the \(a\)-cell.

supervillain.lattice.wedge(a, b)[source]

Wedge product of an n-form a and an m-form b, an (n+m)-form.

For each output component \(O = (o_0, \ldots, o_{n+m-1})\), the sum over all shuffles \(O = A \sqcup B\) (\(A\) the \(n\) \(a\)-directions, \(B\) the \(m\) \(b\)-directions):

\[(a \wedge b)_O[x] = \sum_{O = A \sqcup B} \sigma(A \frown B) \; a_A[x] \; b_B[x + \hat{e}_A]\]

where \(\hat{e}_A = \sum_{k \in A} \hat{e}_k\) and \(\sigma(A \frown B) = (-1)^{\#\{(k, j) \in A \times B \,:\, j < k\}}\) is the sign of the permutation sorting \((A \frown B)\) back into \(O\).

Parameters
  • a (Form) – An n-form on a Lattice.

  • b (Form) – An m-form on the same Lattice.

Returns

The (n+m)-form \(a \wedge b\).

Return type

Form

Raises

ValueError – If \(n + m > D\).

The wedge product is bilinear,

\[(a+b) \wedge c = a \wedge c + b \wedge c a \wedge (b+c) = a \wedge b + a \wedge c\]

which is checked by the test_wedge_bilinear test in test/test_lattice.py, and it is associative,

\[(a \wedge b) \wedge c = a \wedge (b \wedge c)\]

which is checked by the test_wedge_associative test in test/test_lattice.py. The Leibniz rule

\[d(a \wedge b) = da \wedge b + (-1)^{\deg a}\, a \wedge db\]

holds exactly, which is checked by the test_leibniz_rule test in test/test_lattice.py.

The wedge also satisfies the inner product identity

\[\sum_x a_x b_x = \sum_{x} (a \wedge \star b)_{(0, \ldots, D-1)}[x]\]

which holds exactly, as checked by the test_wedge_hodge_inner_product test in test/test_lattice.py.

Unlike the continuum, however, the lattice wedge product is not anti-commutative; see the note below.

Differences from the Continuum

In the continuum, on a Riemannian manifold with Euclidean signature, the Hodge star squares to

\[\star\star a = (-1)^{p(D-p)}\, a \qquad \text{on a } p\text{-form.}\]

Danger

On the lattice the identity picks up a spatial shift,

\[(\star\star f)_I[x] = (-1)^{p(D-p)}\, f_I\!\left[x - \hat{e}_{\mathrm{all}}\right]\]

a backward shift of \(\hat{e}_{\mathrm{all}} = \sum_\mu \hat{e}_\mu\) — one step in every direction. The shift drops out of any periodic sum. This is tested by test_star_star in test/test_lattice.py.

In the continuum we expect anti/commutativity

\[(a \wedge b) = (-1)^{n m} (b \wedge a)\]

to hold.

Danger

On the lattice this property fails!

In the continuum, on a Riemannian manifold with Euclidean signature, the Hodge star satisfies

\[\delta = (-1)^{D(k+1)+1}\, \star\, d\, \star \qquad \text{on } k\text{-forms}.\]

Danger

On the lattice the identity picks up a spatial shift,

\[\delta = (-1)^{D(k+1)+1}\, T_{\hat{e}_{\mathrm{all}}}\, \star\, d\, \star\]

where \(\hat{e}_{\mathrm{all}} = \sum_\mu \hat{e}_\mu\) and \(T_{\Delta x}\) is the translation operator in pull() by \(\Delta x\). The shift by \(\hat{e}_{\mathrm{all}}\) drops out of any periodic sum, which is why the adjoint identity (1) holds exactly despite it. This is tested by test_star_d_star_equals_shifted_delta in test/test_lattice.py.

The translation commutes with the other operations, as tested in test_star_d_star_equals_delta_shifted in test/test_lattice.py.