How to continue (vector calculus) abuse of notation in Julia?

Is there an idiomatic way to get the curl of a symbolic vector field using Symbolics.jl? I’m trying the following

using Symbolics, LinearAlgebra
@variables x y z
@variables A₁(x,y,z) A₂(x,y,z) A₃(x,y,z)
∇ = [Differential(x), Differential(y), Differential(z)]
A = [A₁, A₂, A₃]
∇ × A

But this errors with MethodError: no method matching -(::ComposedFunction{Differential, Num}, ::ComposedFunction{Differential, Num}) since × is understandably a multiplicative operator, so multiplying ∂/∂x to A_x gives Differential(x, 1) ∘ A_x(x, y, z) instead of applying the derivative operator like we’re used to in math. I can get around this by doing some type piracy, mainly, defining

Base.:(*)(D::Differential, x::Num) = D(x)

but I don’t know if there’s a more elegant way without hooking into Base or Symbolics internals.

The above example talks about curls, but really, I want to be able to treat the gradient operator as a “vector” and use it in the same way in math. That is, create a “vector” operator out of any set of Cartesian coordinates in Euclidean space, then apply it to vector fields. For example, I’m also trying the possibility of a primed frame, where:

@variables x′ y′ z′
@variables W₁(x′,y′,z′) W₂(x′,y′,z′) W₃(x′,y′,z′)
∇′ = [Differential(x′), Differential(y′), Differential(z′)]
W = [W₁, W₂, W₃]
∇′ ⋅ W # divergence of W in the primed frame
4 Likes

Generally speaking, if you want custom syntax that’s not straightforward to implement via operator overloading, you need to resort to macros in Julia. (In languages without macros, you write a custom parser for your syntax extension.)

1 Like

Should be better to define your own type, which makes it fine to overload any operators, and not muck with any internalsw. I’d also prefer not to overload * but rather at higher level of ×. Here’s a janky proof of concept.

using Symbolics

struct Nabla
    x::Differential
    y::Differential
    z::Differential
end
# Alternate constructor
Nabla(x::Symbolics.Num, y::Symbolics.Num, z::Symbolics.Num) = Nabla(Differential(x), Differential(y), Differential(z))
# Functor to be applied to scalar symbolic expressions
(d::Nabla)(f)=[d.x(f),d.y(f),d.z(f)]

@variables x y z

∇ = Nabla(x,y,z)
×(d::Nabla,F) = [d.x(F[3])-d.z(F[2]),d.z(F[1])-d.x(F[3]),d.x(F[2])-d.y(F[1])]
dot(d::Nabla,F) = [d.x(F[1]),d.y(F[2]),d.z(F[3])]
⋅(a,b) = dot(a,b)

Example:

julia> ∇(x^2 + 3y + z) # gradient of scalar expression
3-element Vector{Num}:
 Differential(x, 1)(3y + z + x^2)
 Differential(y, 1)(3y + z + x^2)
 Differential(z, 1)(3y + z + x^2)

julia> ∇⋅[x^2 + 3y + z, y^2, z] # divergence of vector field
3-element Vector{Num}:
 Differential(x, 1)(3y + z + x^2)
          Differential(y, 1)(y^2)
            Differential(z, 1)(z)

julia> ∇×[x^2 + 3y + z, y^2, z] # curl of vector field
3-element Vector{Num}:
            Differential(x, 1)(z) - Differential(z, 1)(y^2)
  -Differential(x, 1)(z) + Differential(z, 1)(3y + z + x^2)
 Differential(x, 1)(y^2) - Differential(y, 1)(3y + z + x^2)

Probably better to express vector field as its own type rather than an array like this, but this at least shows how the nabla could be implemented without type piracy.

I’ve often been drawn to cool Unicode symbols, but usually regret it afterwards because they feel like party tricks. They make for cool looking notebooks that are initially pleasing to the eye. But I often feel some anxiety just reading my own notebooks a few weeks later, because I can’t remember how to type the symbols, and even if I do, my VS Code setup often disables Unicode tab completion in favor of some “smarter” AI autocomplete. In the end I just spell out “alpha” and “curl()”.

2 Likes