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.