Skip to content

Symbolic and numeric

TensND is generic in its element type. The same code paths run on Float64, on ForwardDiff.Dual, on SymPy's Sym and on Symbolics' Num, which is what lets a derivation be done symbolically and the result evaluated — or differentiated — numerically without rewriting anything.

The four scalar worlds

Element typeComes fromUse for
Float64 (and any Real)Basenumerical work
ForwardDiff.DualForwardDiffderivatives and sensitivities
SymSymPysymbolic derivation, dsolve, solve
NumSymbolicsnative Julia CAS, code generation
julia
using TensND, SymPy

k, μ = symbols("k μ", positive = true)
= TensISO{3}(3k, 2μ)
get_data(inv(ℂ))
(1/(3*k), 1/(2*μ))

The same with Symbolics:

julia
using TensND, Symbolics

Symbolics.@variables kn μn
get_data(inv(TensISO{3}(3kn, 2μn)))
(1 / (3kn), 1 / (2μn))

What is generic, and what is not

tensor algebrastructured typesprojection, fixed axisprojection, optimizedsymbolic operatorsnumerical operators
Float64
ForwardDiff.Dual
Sym
Numpartial

The one genuine restriction is the orientation search: it is a numerical optimization, so it needs a numeric element type. Projection onto a class with a given axis or frame is closed-form and works symbolically.

Symbolic helpers

Six functions apply a symbolic operation elementwise to a tensor, an array or a scalar, and are no-ops on numeric types — so generic code can call them unconditionally:

FunctionDoesSymPySymbolics
tsimplifysimplify
tfactorfactorize
tsubssubstitute
tdiffdifferentiate
ttrigsimptrigonometric simplification
texpand_trigtrigonometric expansion
julia
tsimplify(3.0), tsimplify(k + k)
(3.0, 2*k)

Being no-ops on Float64 is the point: a function written with tsimplify sprinkled through it runs unchanged, and at full speed, on numeric input.

Differentiating

ForwardDiff composes with the tensor algebra, the structured types and the projections:

julia
using TensND, ForwardDiff, LinearAlgebra

n = [0.0, 0.0, 1.0]
f(α) = get_data(proj_tens(:TI, get_array(tens_TI(10.0, 3.0, 2.5, 12.0, 2.0, [sin(α), 0.0, cos(α)])), n)[1])[1]

ad = ForwardDiff.derivative(f, 0.3)
fd = (f(0.3 + 1.0e-6) - f(0.3 - 1.0e-6)) / 2.0e-6
(ad, fd)
(-5.32346083364259, -5.323460833928095)

and with the numerical differential operators, in both directions — with respect to the evaluation point and with respect to a parameter carried by the field. See Numerical coordinate systems.

Interoperability with Tensors.jl

TensND stores its data in Tensors.jl containers whenever the shape allows, so the two compose directly and index symmetries are preserved:

julia
using TensND, Tensors, LinearAlgebra

st = SymmetricTensor{2, 3}((i, j) -> Float64(i + j))
t = Tens(st)
typeof(get_array(t))
Tensors.SymmetricTensor{2, 3, Float64, 6}
julia
C4 = SymmetricTensor{4, 3}((i, j, k, l) -> Float64(i + j + k + l))
norm(get_array(inv_KM(KM(Tens(C4)))) - get_array(Tens(C4)))
2.5121479338940403e-15

Performance notes

Symbolic element types are orders of magnitude slower than numeric ones, and expression size — not operation count — dominates. Three practical consequences:

  • simplify early and often. An unsimplified intermediate propagates into every later expression. This is what the rules / tmp_var / to_coords machinery of CoorSystemSym exists for.

  • derive symbolically, evaluate numerically. Obtain the closed form once, then substitute with tsubs or generate a numeric function from it.

  • prefer a structured type over a dense array whenever the physics allows — the measured gains are one to three orders of magnitude, see Performance of the structured types.

For pointwise numerical work, CoorSystemNum avoids the symbolic layer entirely and costs a constant amount per point.