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
OperatorContextowns 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.
Expected Result
The snippet fills a scalar field on the vertices, computes dφ, 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 = 0still 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.