Skip to content

Step 2 — Exterior Derivative and dd = 0

Capability

Assemble the exterior derivative on a tiny mesh and verify that applying it twice yields zero.

Problem Statement

The first structural identity to internalize is dd = 0. The code should make it obvious which map is d₀, which map is d₁, and why the second application lands in face space.

Mathematical Idea

In a simplicial complex, the oriented boundary of a boundary vanishes. Dual language, commuting diagrams, and deeper FEEC structure all build on this.

Flux Concepts

  • OperatorContext owns assembled DEC operators for one mesh.
  • exteriorDerivative(Primal, 0) maps primal 0-forms to primal 1-forms.
  • exteriorDerivative(Primal, 1) maps primal 1-forms to primal 2-forms.

Minimal Runnable Snippet

Commented walkthrough:

const std = @import("std"); const flux = @import("flux"); pub fn run(allocator: std.mem.Allocator) !void { const Mesh2D = flux.topology.Mesh(2, 2); var mesh = try Mesh2D.plane(allocator, 2, 2, 1.0, 1.0); defer mesh.deinit(allocator); const OperatorContext = flux.operators.context.OperatorContext(Mesh2D); var operators = try OperatorContext.init(allocator, &mesh); defer operators.deinit(); var phi = try flux.forms.Cochain(Mesh2D, 0, flux.forms.Primal).init(allocator, &mesh); defer phi.deinit(allocator); phi.values[0] = 1.0; phi.values[1] = -0.25; phi.values[2] = 0.5; phi.values[3] = 0.75; // d₀: primal 0-forms -> primal 1-forms. var d_phi = try (try operators.exteriorDerivative(flux.forms.Primal, 0)).apply(allocator, phi); defer d_phi.deinit(allocator); // d₁(d₀ φ) should vanish exactly on every face. var dd_phi = try (try operators.exteriorDerivative(flux.forms.Primal, 1)).apply(allocator, d_phi); defer dd_phi.deinit(allocator); for (dd_phi.values) |value| { try std.testing.expectApproxEqAbs(0.0, value, 1e-12); } } test "step 02 commented snippet compiles and proves dd = 0 on a tiny mesh" { try run(std.testing.allocator); }

Plain program:

const std = @import("std"); const flux = @import("flux"); pub fn run(allocator: std.mem.Allocator) !void { const Mesh2D = flux.topology.Mesh(2, 2); var mesh = try Mesh2D.plane(allocator, 2, 2, 1.0, 1.0); defer mesh.deinit(allocator); var operators = try flux.operators.context.OperatorContext(Mesh2D).init(allocator, &mesh); defer operators.deinit(); var phi = try flux.forms.Cochain(Mesh2D, 0, flux.forms.Primal).init(allocator, &mesh); defer phi.deinit(allocator); phi.values[0] = 1.0; phi.values[1] = -0.25; phi.values[2] = 0.5; phi.values[3] = 0.75; var d_phi = try (try operators.exteriorDerivative(flux.forms.Primal, 0)).apply(allocator, phi); defer d_phi.deinit(allocator); var dd_phi = try (try operators.exteriorDerivative(flux.forms.Primal, 1)).apply(allocator, d_phi); defer dd_phi.deinit(allocator); for (dd_phi.values) |value| { try std.testing.expectApproxEqAbs(0.0, value, 1e-12); } } test "step 02 plain snippet compiles and runs" { try run(std.testing.allocator); }

Interactive toy complex:

This guided playground keeps the kernel intentionally tiny: a single oriented triangle, a scalar 0-form on vertices, and the discrete exterior derivative applied twice. The kernel itself runs in WebAssembly compiled from Zig.

Playground status
Loading the Zig WebAssembly kernel…
dφ on oriented edges
loading…
d(dφ) on the face
loading…

Expected Result

The snippet fills a scalar field on the vertices, computes , then computes d(dφ). The final face cochain is checked against zero to machine precision.

Possible Extensions

  • Replace the vertex values and verify that dd = 0 still holds.
  • Read the tiny triangle playground next to the full mesh snippet and compare the two viewpoints.
  • Once 3D operators are the topic, repeat the same reasoning with tetrahedra.

API Reference Jump

See flux.operators.context.OperatorContext and flux.operators.exterior_derivative.