Skip to content

Step 3 — Hodge Star Intuition

Capability

Apply the Hodge star and its inverse to see where topology stops and metric structure begins.

Problem Statement

After d, the next essential operator is . The teaching goal here is not a full derivation; it is to make the degree and duality transition legible in code.

Mathematical Idea

In 2D, maps primal 1-forms to dual 1-forms. This is the first place where metric structure matters directly, so it should feel different from incidence-based d.

Flux Concepts

  • hodgeStar(1) changes primal degree-1 storage into dual degree-1 storage.
  • hodgeStarInverse(1) reverses that mapping.
  • This is the surface where DEC geometry and FEEC mass-matrix language meet.

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); var operators = try flux.operators.context.OperatorContext(Mesh2D).init(allocator, &mesh); defer operators.deinit(); var primal_one = try flux.forms.Cochain(Mesh2D, 1, flux.forms.Primal).init(allocator, &mesh); defer primal_one.deinit(allocator); for (primal_one.values, 0..) |*value, index| { value.* = @as(f64, @floatFromInt(index + 1)); } // ★ maps primal 1-forms to dual 1-forms in 2D. var dual_one = try (try operators.hodgeStar(1)).apply(allocator, primal_one); defer dual_one.deinit(allocator); // ★⁻¹ should recover the primal field. var recovered = try (try operators.hodgeStarInverse(1)).apply(allocator, dual_one); defer recovered.deinit(allocator); for (primal_one.values, recovered.values) |expected, actual| { try std.testing.expectApproxEqAbs(expected, actual, 1e-9); } } test "step 03 commented snippet compiles and runs" { 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 primal_one = try flux.forms.Cochain(Mesh2D, 1, flux.forms.Primal).init(allocator, &mesh); defer primal_one.deinit(allocator); for (primal_one.values, 0..) |*value, index| value.* = @as(f64, @floatFromInt(index + 1)); var dual_one = try (try operators.hodgeStar(1)).apply(allocator, primal_one); defer dual_one.deinit(allocator); var recovered = try (try operators.hodgeStarInverse(1)).apply(allocator, dual_one); defer recovered.deinit(allocator); for (primal_one.values, recovered.values) |expected, actual| { try std.testing.expectApproxEqAbs(expected, actual, 1e-9); } } test "step 03 plain snippet compiles and runs" { try run(std.testing.allocator); }

Expected Result

The example populates a primal 1-form, applies , then applies ★⁻¹, and checks that the original coefficients are recovered.

Possible Extensions

  • Compare the storage lengths of the primal and dual forms on the same mesh.
  • Read the Whitney mass implementation when you want the FEEC side of the story.
  • Once metric parameterization lands, revisit which part of the operator changes and which part does not.

API Reference Jump

See flux.operators.hodge_star and flux.operators.feec.whitney_mass.