From eff01c13fe424952afdd0f32d1d943b056077f9d Mon Sep 17 00:00:00 2001 From: Jeffrey Ward Date: Sat, 11 Jul 2026 18:29:11 -0400 Subject: [PATCH] feat: spell the wedge product `^`, as Python does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `wedge(α, β)` was the only spelling, while `*` on two Forms is the tensor product — so Python code ported by eye silently produced a Tensor02 instead of a Form. Add `^` as the wedge (and accept a ScalarFunction on either side, where it degenerates to the pointwise product), and document the `*` divergence plus the `^` precedence flip in the README. `^` on two ScalarFunctions is deliberately left undefined: it would collide in meaning with the pointwise power `f^2`. Closes #14 --- README.md | 12 ++++++++++++ src/tensors/forms/forms.jl | 33 ++++++++++++++++++++++++++++++++- test/test_tensor_algebra.jl | 14 ++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b25e59..3fe0742 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,13 @@ S = dg_tensor02sym(dg, randn(200, 6)) # symmetric (0,2)-tensor, (n, d(d+1) The field types — `ScalarFunction`, `VectorField`, `Form`, `Tensor02`, `Tensor02Sym`, and `DirectSumElement` — support arithmetic, wedge and tensor products, `symmetrise`/`transpose_tensor`, and musical isomorphisms `sharp`/`flat`. + +```julia +wedge(α, β) # wedge product, degree k₁ + k₂ — also spelled α ^ β +α * β # ⚠ tensor product of two 1-forms → Tensor02, not the wedge +f * α # pointwise product with a function (so is wedge(f, α)) +``` + The learned metric gives you geometry: ```julia @@ -258,6 +265,11 @@ available directly. Animate a time-evolving field with `dganimate`. See in keeping with Julia. Point-cloud data is `n × d` (rows are points). - Arrays keep a leading point axis `p`, so a field's coefficients are laid out per point. +- **`*` on two `Form`s is the tensor product**, following Python; the wedge is + `wedge(α, β)` or `α ^ β`. Julia's `^` binds *tighter* than `+` and `*` where + Python's binds looser than both, so a Python expression that leans on that + precedence changes meaning when copied across: `α ^ β + γ` is `(α ∧ β) + γ` + here, but `α ∧ (β + γ)` in Python. Parenthesise when porting. ## Performance diff --git a/src/tensors/forms/forms.jl b/src/tensors/forms/forms.jl index f7fe042..b943daa 100644 --- a/src/tensors/forms/forms.jl +++ b/src/tensors/forms/forms.jl @@ -42,9 +42,13 @@ end # ── Wedge product α ∧ β : Ωᵏ¹ × Ωᵏ² → Ωᵏ¹⁺ᵏ² ────────────────────────────────── """ wedge(a, b) -> Form + a ^ b -> Form Wedge product of two forms, `(α ∧ β)_K = Σ_{I∪J=K} sgn(I,J) α_I β_J`. Returns a zero function when the combined degree exceeds the ambient dimension. + +`^` is the same product, spelled as in Python. Beware that `*` on two `Form`s is +the *tensor* product, not the wedge. """ function wedge(a::Form, b::Form) dg = geometry(a) @@ -77,8 +81,35 @@ function wedge(a::Form, b::Form) return dg_form(dg, out_pw, ktot) end +""" + wedge(f::ScalarFunction, ω::Form) -> Form + wedge(ω::Form, f::ScalarFunction) -> Form + +A function is a degree-0 form, so the wedge with one degenerates to the pointwise +product (as in Python). There is deliberately no `wedge` on two `ScalarFunction`s: +`f^g` already reads as a pointwise power, and `Base.:^(::ScalarFunction, ::Number)` +is that power — writing `f * g` for the product keeps `^` unambiguous. +""" +wedge(f::ScalarFunction, ω::Form) = f * ω +wedge(ω::Form, f::ScalarFunction) = ω * f + +""" + a ^ b -> Form + +The wedge product, spelled as in Python. `^` is otherwise unused on `Form`, and +the pointwise power `Base.:^(::ScalarFunction, ::Number)` is a distinct method. +Julia's `^` binds tighter than `+` and `*`, where Python's binds looser than both: +`α ^ β + γ` is `(α ∧ β) + γ` here but `α ∧ (β + γ)` in Python. +""" +Base.:^(a::Form, b::Form) = wedge(a, b) +Base.:^(f::ScalarFunction, ω::Form) = wedge(f, ω) +Base.:^(ω::Form, f::ScalarFunction) = wedge(ω, f) + # ── Tensor product of 1-forms → general (0,2)-tensor ─────────────────────────── -"""Tensor product of two 1-forms, `(α ⊗ β)_{ij} = α_i β_j`, giving a `Tensor02`.""" +""" +Tensor product of two 1-forms, `(α ⊗ β)_{ij} = α_i β_j`, giving a `Tensor02`. +This is *not* the wedge product — that is `wedge(α, β)`, or `α ^ β`. +""" function Base.:*(a::Form, b::Form) dg = geometry(a) @assert dg === geometry(b) "Forms must be defined on the same DiffusionGeometry." diff --git a/test/test_tensor_algebra.jl b/test/test_tensor_algebra.jl index ae0e97d..6b50289 100644 --- a/test/test_tensor_algebra.jl +++ b/test/test_tensor_algebra.jl @@ -77,6 +77,20 @@ @testset "wedge and tensor products" begin @test aeq(wedge(a1, b1).coeffs, fx["wedge_ab_coeffs"]) @test aeq((a1 * b1).coeffs, fx["tensorprod_ab_coeffs"]) + + # `^` is the Python spelling of the wedge (`*` on two Forms stays the + # tensor product, as in Python). + @test aeq((a1^b1).coeffs, fx["wedge_ab_coeffs"]) + @test degree(a1^b1) == 2 + + # A Function is a degree-0 form: the wedge degenerates to a pointwise + # product, as in Python. + @test aeq(wedge(f, a1).coeffs, (f * a1).coeffs) + @test aeq((f^a1).coeffs, (f * a1).coeffs) + @test aeq((a1^f).coeffs, (a1 * f).coeffs) + # …while `^` on two Functions stays the pointwise power, not a product. + @test aeq((f^2).coeffs, (f * f).coeffs) + @test_throws MethodError wedge(f, h) end @testset "symmetrise / expand / transpose" begin